Skip to content

Commit 6f3dda4

Browse files
Merge pull request #285 from JordonPhillips/ssdk-response-tests
Add ssdk response tests
2 parents 505b142 + fb5b097 commit 6f3dda4

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/HttpProtocolTestGenerator.java

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ private void generateServerOperationTests(OperationShape operation, OperationInd
177177
onlyIfProtocolMatches(testCase, () -> generateServerRequestTest(operation, testCase));
178178
}
179179
});
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+
});
180186
}
181187
}
182188

@@ -242,7 +248,7 @@ private void generateClientRequestTest(OperationShape operation, HttpRequestTest
242248
+ " }\n"
243249
+ " const r = err.request;")
244250
.indent()
245-
.call(() -> writeRequestAssertions(operation, testCase))
251+
.call(() -> writeHttpRequestAssertions(testCase))
246252
.dedent()
247253
.write("}");
248254
});
@@ -349,16 +355,22 @@ private ObjectNode buildQueryBag(HttpRequestTestCase testCase) {
349355
}
350356

351357
// Ensure that the serialized request matches the expected request.
352-
private void writeRequestAssertions(OperationShape operation, HttpRequestTestCase testCase) {
358+
private void writeHttpRequestAssertions(HttpRequestTestCase testCase) {
353359
writer.write("expect(r.method).toBe($S);", testCase.getMethod());
354360
writer.write("expect(r.path).toBe($S);", testCase.getUri());
355361

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);
359371
}
360372

361-
private void writeRequestQueryAssertions(HttpRequestTestCase testCase) {
373+
private void writeHttpQueryAssertions(HttpRequestTestCase testCase) {
362374
testCase.getRequireQueryParams().forEach(requiredQueryParam ->
363375
writer.write("expect(r.query[$S]).toBeDefined();", requiredQueryParam));
364376
writer.write("");
@@ -380,7 +392,7 @@ private void writeRequestQueryAssertions(HttpRequestTestCase testCase) {
380392
writer.write("");
381393
}
382394

383-
private void writeRequestHeaderAssertions(HttpRequestTestCase testCase) {
395+
private void writeHttpHeaderAssertions(HttpMessageTestCase testCase) {
384396
testCase.getRequireHeaders().forEach(requiredHeader -> {
385397
writer.write("expect(r.headers[$S]).toBeDefined();", requiredHeader.toLowerCase());
386398
});
@@ -398,7 +410,7 @@ private void writeRequestHeaderAssertions(HttpRequestTestCase testCase) {
398410
writer.write("");
399411
}
400412

401-
private void writeRequestBodyAssertions(OperationShape operation, HttpRequestTestCase testCase) {
413+
private void writeHttpBodyAssertions(HttpMessageTestCase testCase) {
402414
testCase.getBody().ifPresent(body -> {
403415
// If we expect an empty body, expect it to be falsy.
404416
if (body.isEmpty()) {
@@ -413,6 +425,17 @@ private void writeRequestBodyAssertions(OperationShape operation, HttpRequestTes
413425
String mediaType = testCase.getBodyMediaType().orElse("UNKNOWN");
414426
String comparatorInvoke = registerBodyComparatorStub(mediaType);
415427

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+
416439
// Handle escaping strings with quotes inside them.
417440
writer.write("const bodyString = `$L`;", body.replace("\"", "\\\""));
418441
writer.write("const unequalParts: any = $L;", comparatorInvoke);
@@ -436,19 +459,83 @@ private String registerBodyComparatorStub(String mediaType) {
436459
additionalStubs.add("protocol-test-xml-stub.ts");
437460
return "compareEquivalentXmlBodies(bodyString, r.body.toString())";
438461
case "application/octet-stream":
462+
writer.addImport("Encoder", "__Encoder", "@aws-sdk/types");
439463
additionalStubs.add("protocol-test-octet-stream-stub.ts");
440-
return "compareEquivalentOctetStreamBodies(client.config, bodyString, r.body)";
464+
return "compareEquivalentOctetStreamBodies(utf8Encoder, bodyString, r.body)";
441465
case "text/plain":
442466
additionalStubs.add("protocol-test-text-stub.ts");
443467
return "compareEquivalentTextBodies(bodyString, r.body)";
444468
default:
445469
LOGGER.warning("Unable to compare bodies with unknown media type `" + mediaType
446470
+ "`, defaulting to direct comparison.");
471+
writer.addImport("Encoder", "__Encoder", "@aws-sdk/types");
447472
additionalStubs.add("protocol-test-unknown-type-stub.ts");
448-
return "compareEquivalentUnknownTypeBodies(client.config, bodyString, r.body)";
473+
return "compareEquivalentUnknownTypeBodies(utf8Encoder, bodyString, r.body)";
449474
}
450475
}
451476

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+
452539
private void generateResponseTest(OperationShape operation, HttpResponseTestCase testCase) {
453540
testCase.getDocumentation().ifPresent(writer::writeDocs);
454541
String testName = testCase.getId() + ":Response";
@@ -657,10 +744,16 @@ private void writeParamAssertions(
657744
private final class CommandInputNodeVisitor implements NodeVisitor<Void> {
658745
private final StructureShape inputShape;
659746
private Shape workingShape;
747+
private boolean appendSemicolon;
660748

661749
private CommandInputNodeVisitor(StructureShape inputShape) {
750+
this(inputShape, false);
751+
}
752+
753+
private CommandInputNodeVisitor(StructureShape inputShape, boolean appendSemicolon) {
662754
this.inputShape = inputShape;
663755
this.workingShape = inputShape;
756+
this.appendSemicolon = appendSemicolon;
664757
}
665758

666759
@Override
@@ -716,10 +809,25 @@ public Void objectNode(ObjectNode node) {
716809

717810
// Both objects and maps can use a majority of the same logic.
718811
// 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, () -> {
720824
Shape wrapperShape = this.workingShape;
721825
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+
}
723831

724832
// Grab the correct member related to the node member we have.
725833
MemberShape memberShape;

smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/base-package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"downlevel-dts": "0.7.0",
3434
"jest": "^26.1.0",
3535
"rimraf": "^3.0.0",
36-
"typedoc": "^0.19.2",
36+
"typedoc": "^0.20.0",
3737
"typescript": "~4.1.2"
3838
},
3939
"engines": {

smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/protocol-test-octet-stream-stub.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
* discrepancies between the components.
44
*/
55
const compareEquivalentOctetStreamBodies = (
6-
config: any,
6+
utf8Encoder: __Encoder,
77
expectedBody: string,
88
generatedBody: Uint8Array
99
): Object => {
1010
const expectedParts = {Value: expectedBody};
11-
const generatedParts = {Value: config.utf8Encoder(generatedBody)};
11+
const generatedParts = {Value: utf8Encoder(generatedBody)};
1212

1313
return compareParts(expectedParts, generatedParts);
1414
}

smithy-typescript-codegen/src/main/resources/software/amazon/smithy/typescript/codegen/protocol-test-unknown-type-stub.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
* discrepancies between the components.
44
*/
55
const compareEquivalentUnknownTypeBodies = (
6-
config: any,
6+
utf8Encoder: __Encoder,
77
expectedBody: string,
88
generatedBody: string | Uint8Array
99
): Object => {
1010
const expectedParts = {Value: expectedBody};
1111
const generatedParts = {
12-
Value: generatedBody instanceof Uint8Array ? config.utf8Encoder(generatedBody) : generatedBody
12+
Value: generatedBody instanceof Uint8Array ? utf8Encoder(generatedBody) : generatedBody
1313
};
1414

1515
return compareParts(expectedParts, generatedParts);

0 commit comments

Comments
 (0)