@@ -177,6 +177,12 @@ private void generateServerOperationTests(OperationShape operation, OperationInd
177
177
onlyIfProtocolMatches (testCase , () -> generateServerRequestTest (operation , testCase ));
178
178
}
179
179
});
180
+ // 2. Generate test cases for each response.
181
+ operation .getTrait (HttpResponseTestsTrait .class ).ifPresent (trait -> {
182
+ for (HttpResponseTestCase testCase : trait .getTestCasesFor (AppliesTo .SERVER )) {
183
+ onlyIfProtocolMatches (testCase , () -> generateServerResponseTest (operation , testCase ));
184
+ }
185
+ });
180
186
}
181
187
}
182
188
@@ -242,7 +248,7 @@ private void generateClientRequestTest(OperationShape operation, HttpRequestTest
242
248
+ " }\n "
243
249
+ " const r = err.request;" )
244
250
.indent ()
245
- .call (() -> writeRequestAssertions ( operation , testCase ))
251
+ .call (() -> writeHttpRequestAssertions ( testCase ))
246
252
.dedent ()
247
253
.write ("}" );
248
254
});
@@ -349,16 +355,22 @@ private ObjectNode buildQueryBag(HttpRequestTestCase testCase) {
349
355
}
350
356
351
357
// Ensure that the serialized request matches the expected request.
352
- private void writeRequestAssertions ( OperationShape operation , HttpRequestTestCase testCase ) {
358
+ private void writeHttpRequestAssertions ( HttpRequestTestCase testCase ) {
353
359
writer .write ("expect(r.method).toBe($S);" , testCase .getMethod ());
354
360
writer .write ("expect(r.path).toBe($S);" , testCase .getUri ());
355
361
356
- writeRequestHeaderAssertions (testCase );
357
- writeRequestQueryAssertions (testCase );
358
- writeRequestBodyAssertions (operation , testCase );
362
+ writeHttpHeaderAssertions (testCase );
363
+ writeHttpQueryAssertions (testCase );
364
+ writeHttpBodyAssertions (testCase );
365
+ }
366
+
367
+ private void writeHttpResponseAssertions (HttpResponseTestCase testCase ) {
368
+ writer .write ("expect(r.statusCode).toBe($L);" , testCase .getCode ());
369
+ writeHttpHeaderAssertions (testCase );
370
+ writeHttpBodyAssertions (testCase );
359
371
}
360
372
361
- private void writeRequestQueryAssertions (HttpRequestTestCase testCase ) {
373
+ private void writeHttpQueryAssertions (HttpRequestTestCase testCase ) {
362
374
testCase .getRequireQueryParams ().forEach (requiredQueryParam ->
363
375
writer .write ("expect(r.query[$S]).toBeDefined();" , requiredQueryParam ));
364
376
writer .write ("" );
@@ -380,7 +392,7 @@ private void writeRequestQueryAssertions(HttpRequestTestCase testCase) {
380
392
writer .write ("" );
381
393
}
382
394
383
- private void writeRequestHeaderAssertions ( HttpRequestTestCase testCase ) {
395
+ private void writeHttpHeaderAssertions ( HttpMessageTestCase testCase ) {
384
396
testCase .getRequireHeaders ().forEach (requiredHeader -> {
385
397
writer .write ("expect(r.headers[$S]).toBeDefined();" , requiredHeader .toLowerCase ());
386
398
});
@@ -398,7 +410,7 @@ private void writeRequestHeaderAssertions(HttpRequestTestCase testCase) {
398
410
writer .write ("" );
399
411
}
400
412
401
- private void writeRequestBodyAssertions ( OperationShape operation , HttpRequestTestCase testCase ) {
413
+ private void writeHttpBodyAssertions ( HttpMessageTestCase testCase ) {
402
414
testCase .getBody ().ifPresent (body -> {
403
415
// If we expect an empty body, expect it to be falsy.
404
416
if (body .isEmpty ()) {
@@ -413,6 +425,17 @@ private void writeRequestBodyAssertions(OperationShape operation, HttpRequestTes
413
425
String mediaType = testCase .getBodyMediaType ().orElse ("UNKNOWN" );
414
426
String comparatorInvoke = registerBodyComparatorStub (mediaType );
415
427
428
+ // If this is a request case then we know we're generating a client test,
429
+ // because a request case for servers would be comparing parsed objects. We
430
+ // need to know which is which here to be able to grab the utf8Encoder from
431
+ // the right place.
432
+ if (testCase instanceof HttpRequestTestCase ) {
433
+ writer .write ("const utf8Encoder = client.config.utf8Encoder;" );
434
+ } else {
435
+ writer .addImport ("toUtf8" , "__utf8Encoder" , "@aws-sdk/util-utf8-node" );
436
+ writer .write ("const utf8Encoder = __utf8Encoder;" );
437
+ }
438
+
416
439
// Handle escaping strings with quotes inside them.
417
440
writer .write ("const bodyString = `$L`;" , body .replace ("\" " , "\\ \" " ));
418
441
writer .write ("const unequalParts: any = $L;" , comparatorInvoke );
@@ -436,19 +459,83 @@ private String registerBodyComparatorStub(String mediaType) {
436
459
additionalStubs .add ("protocol-test-xml-stub.ts" );
437
460
return "compareEquivalentXmlBodies(bodyString, r.body.toString())" ;
438
461
case "application/octet-stream" :
462
+ writer .addImport ("Encoder" , "__Encoder" , "@aws-sdk/types" );
439
463
additionalStubs .add ("protocol-test-octet-stream-stub.ts" );
440
- return "compareEquivalentOctetStreamBodies(client.config , bodyString, r.body)" ;
464
+ return "compareEquivalentOctetStreamBodies(utf8Encoder , bodyString, r.body)" ;
441
465
case "text/plain" :
442
466
additionalStubs .add ("protocol-test-text-stub.ts" );
443
467
return "compareEquivalentTextBodies(bodyString, r.body)" ;
444
468
default :
445
469
LOGGER .warning ("Unable to compare bodies with unknown media type `" + mediaType
446
470
+ "`, defaulting to direct comparison." );
471
+ writer .addImport ("Encoder" , "__Encoder" , "@aws-sdk/types" );
447
472
additionalStubs .add ("protocol-test-unknown-type-stub.ts" );
448
- return "compareEquivalentUnknownTypeBodies(client.config , bodyString, r.body)" ;
473
+ return "compareEquivalentUnknownTypeBodies(utf8Encoder , bodyString, r.body)" ;
449
474
}
450
475
}
451
476
477
+ public void generateServerResponseTest (OperationShape operation , HttpResponseTestCase testCase ) {
478
+ Symbol serviceSymbol = serverSymbolProvider .toSymbol (service );
479
+ Symbol operationSymbol = serverSymbolProvider .toSymbol (operation );
480
+ Symbol handlerSymbol = serviceSymbol .expectProperty ("handler" , Symbol .class );
481
+ Symbol serviceOperationsSymbol = serviceSymbol .expectProperty ("operations" , Symbol .class );
482
+ testCase .getDocumentation ().ifPresent (writer ::writeDocs );
483
+ String testName = testCase .getId () + ":ServerResponse" ;
484
+ writer .openBlock ("it($S, async () => {" , "});\n " , testName , () -> {
485
+ Symbol outputType = operationSymbol .expectProperty ("outputType" , Symbol .class );
486
+ writer .openBlock ("class TestService implements Partial<$T> {" , "}" , serviceSymbol , () -> {
487
+ writer .openBlock ("$L(input: any, request: HttpRequest): $T {" , "}" ,
488
+ operationSymbol .getName (), outputType , () -> {
489
+ Optional <ShapeId > outputOptional = operation .getOutput ();
490
+ if (outputOptional .isPresent ()) {
491
+ StructureShape outputShape = model .expectShape (outputOptional .get (), StructureShape .class );
492
+ writer .writeInline ("let response = " );
493
+ testCase .getParams ().accept (new CommandInputNodeVisitor (outputShape , true ));
494
+ writer .write ("return { ...response, '$$metadata': {} };" );
495
+ } else {
496
+ writer .write ("return { '$$metadata': {} };" );
497
+ }
498
+ });
499
+ });
500
+
501
+ writer .write ("const service: any = new TestService()" );
502
+
503
+ // There's a lot of setup here, including creating our own mux, serializers list, and ultimately
504
+ // our own service handler. This is largely in service of avoiding having to go through the
505
+ // request deserializer
506
+ writer .addImport ("httpbinding" , null , "@aws-smithy/server-common" );
507
+ writer .openBlock ("const testMux = new httpbinding.HttpBindingMux<$S, keyof $T>([" , "]);" ,
508
+ service .getId ().getName (), serviceSymbol , () -> {
509
+ writer .openBlock ("new httpbinding.UriSpec<$S, $S>('POST', [], [], {" , "})," ,
510
+ service .getId ().getName (), operation .getId ().getName (), () -> {
511
+ writer .write ("service: $S," , service .getId ().getName ());
512
+ writer .write ("operation: $S," , operation .getId ().getName ());
513
+ });
514
+ });
515
+
516
+ writer .write ("const request = new HttpRequest({method: 'POST', hostname: 'example.com'});" );
517
+
518
+ String serializerName = ProtocolGenerator .getGenericSerFunctionName (operationSymbol ) + "Response" ;
519
+ writer .addImport (serializerName , serializerName ,
520
+ "./protocols/" + ProtocolGenerator .getSanitizedName (protocolGenerator .getName ()));
521
+
522
+ writer .addImport ("OperationSerializer" , "__OperationSerializer" , "@aws-smithy/server-common" );
523
+ writer .openBlock ("const serFn: (op: $1T) => __OperationSerializer<$2T, $1T> = (op) => {" , "};" ,
524
+ serviceOperationsSymbol , serviceSymbol , () -> {
525
+ writer .openBlock ("return {" , "};" , () -> {
526
+ writer .write ("serialize: $L," , serializerName );
527
+ writer .openBlock ("deserialize: (output: any, context: any): Promise<any> => {" , "}," , () -> {
528
+ writer .write ("return Promise.resolve({});" );
529
+ });
530
+ });
531
+ });
532
+
533
+ writer .write ("const handler = new $T(service, testMux, serFn);" , handlerSymbol );
534
+ writer .write ("let r = await handler.handle(request)" ).write ("" );
535
+ writeHttpResponseAssertions (testCase );
536
+ });
537
+ }
538
+
452
539
private void generateResponseTest (OperationShape operation , HttpResponseTestCase testCase ) {
453
540
testCase .getDocumentation ().ifPresent (writer ::writeDocs );
454
541
String testName = testCase .getId () + ":Response" ;
@@ -657,10 +744,16 @@ private void writeParamAssertions(
657
744
private final class CommandInputNodeVisitor implements NodeVisitor <Void > {
658
745
private final StructureShape inputShape ;
659
746
private Shape workingShape ;
747
+ private boolean appendSemicolon ;
660
748
661
749
private CommandInputNodeVisitor (StructureShape inputShape ) {
750
+ this (inputShape , false );
751
+ }
752
+
753
+ private CommandInputNodeVisitor (StructureShape inputShape , boolean appendSemicolon ) {
662
754
this .inputShape = inputShape ;
663
755
this .workingShape = inputShape ;
756
+ this .appendSemicolon = appendSemicolon ;
664
757
}
665
758
666
759
@ Override
@@ -716,10 +809,25 @@ public Void objectNode(ObjectNode node) {
716
809
717
810
// Both objects and maps can use a majority of the same logic.
718
811
// Use "as any" to have TS complain less about undefined entries.
719
- writer .openBlock ("{" , "} as any,\n " , () -> {
812
+ String suffix = "} as any" ;
813
+
814
+ // When generating a server response test, we need the top level structure to have a semicolon
815
+ // rather than a comma.
816
+ if (appendSemicolon ) {
817
+ suffix += ";" ;
818
+ appendSemicolon = false ;
819
+ } else {
820
+ suffix += ",\n " ;
821
+ }
822
+
823
+ writer .openBlock ("{" , suffix , () -> {
720
824
Shape wrapperShape = this .workingShape ;
721
825
node .getMembers ().forEach ((keyNode , valueNode ) -> {
722
- writer .writeInline ("$L: " , keyNode .getValue ());
826
+ if (keyNode .getValue ().matches ("[^\\ w]+" )) {
827
+ writer .writeInline ("$L: " , keyNode .getValue ());
828
+ } else {
829
+ writer .writeInline ("$S: " , keyNode .getValue ());
830
+ }
723
831
724
832
// Grab the correct member related to the node member we have.
725
833
MemberShape memberShape ;
0 commit comments