Skip to content

Commit 2e2664f

Browse files
Merge pull request #293 from JordonPhillips/exception-handling-updates
Generate ssdk error tests
2 parents 1735105 + 67b2a0d commit 2e2664f

File tree

2 files changed

+109
-46
lines changed

2 files changed

+109
-46
lines changed

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

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ private void generateServerOperationTests(OperationShape operation, OperationInd
183183
onlyIfProtocolMatches(testCase, () -> generateServerResponseTest(operation, testCase));
184184
}
185185
});
186+
// 3. Generate test cases for each error on each operation.
187+
for (StructureShape error : operationIndex.getErrors(operation)) {
188+
if (!error.hasTag("client-only")) {
189+
error.getTrait(HttpResponseTestsTrait.class).ifPresent(trait -> {
190+
for (HttpResponseTestCase testCase : trait.getTestCasesFor(AppliesTo.SERVER)) {
191+
onlyIfProtocolMatches(testCase,
192+
() -> generateServerErrorResponseTest(operation, error, testCase));
193+
}
194+
});
195+
}
196+
}
186197
}
187198
}
188199

@@ -288,8 +299,7 @@ private void generateServerRequestTest(OperationShape operation, HttpRequestTest
288299
});
289300

290301
String getHandlerName = "get" + handlerSymbol.getName();
291-
writer.addImport(getHandlerName, getHandlerName,
292-
"./protocols/" + ProtocolGenerator.getSanitizedName(protocolGenerator.getName()));
302+
writer.addImport(getHandlerName, null, "./server/");
293303

294304
// Cast the service as any so TS will ignore the fact that the type being passed in is incomplete.
295305
writer.write("const handler = $L(testService as $T);", getHandlerName, serviceSymbol);
@@ -477,8 +487,6 @@ private String registerBodyComparatorStub(String mediaType) {
477487
public void generateServerResponseTest(OperationShape operation, HttpResponseTestCase testCase) {
478488
Symbol serviceSymbol = serverSymbolProvider.toSymbol(service);
479489
Symbol operationSymbol = serverSymbolProvider.toSymbol(operation);
480-
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
481-
Symbol serviceOperationsSymbol = serviceSymbol.expectProperty("operations", Symbol.class);
482490
testCase.getDocumentation().ifPresent(writer::writeDocs);
483491
String testName = testCase.getId() + ":ServerResponse";
484492
writer.openBlock("it($S, async () => {", "});\n", testName, () -> {
@@ -497,42 +505,7 @@ public void generateServerResponseTest(OperationShape operation, HttpResponseTes
497505
}
498506
});
499507
});
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);
508+
writeServerResponseTest(operation, testCase);
536509
});
537510
}
538511

@@ -554,6 +527,92 @@ private void generateResponseTest(OperationShape operation, HttpResponseTestCase
554527
});
555528
}
556529

530+
private void generateServerErrorResponseTest(
531+
OperationShape operation,
532+
StructureShape error,
533+
HttpResponseTestCase testCase
534+
) {
535+
Symbol serviceSymbol = serverSymbolProvider.toSymbol(service);
536+
Symbol operationSymbol = serverSymbolProvider.toSymbol(operation);
537+
Symbol outputType = operationSymbol.expectProperty("outputType", Symbol.class);
538+
Symbol errorSymbol = serverSymbolProvider.toSymbol(error);
539+
ErrorTrait errorTrait = error.expectTrait(ErrorTrait.class);
540+
541+
testCase.getDocumentation().ifPresent(writer::writeDocs);
542+
String testName = testCase.getId() + ":ServerErrorResponse";
543+
writer.openBlock("it($S, async () => {", "});\n", testName, () -> {
544+
545+
// Generates a Partial implementation of the service type that only includes
546+
// the specific operation under test. Later we'll have to "cast" this with an "as",
547+
// but using the partial in the meantime will give us proper type checking on the
548+
// operation we want.
549+
writer.openBlock("class TestService implements Partial<$T> {", "}", serviceSymbol, () -> {
550+
writer.openBlock("$L(input: any, request: HttpRequest): $T {", "}",
551+
operationSymbol.getName(), outputType, () -> {
552+
// Write out an object according to what's defined in the test case.
553+
writer.writeInline("const response = ");
554+
testCase.getParams().accept(new CommandInputNodeVisitor(error, true));
555+
556+
// Add in the necessary wrapping information to make the error satisfy its interface.
557+
// TODO: having proper constructors for these errors would be really nice so we don't
558+
// have to do this.
559+
writer.openBlock("const error: $T = {", "};", errorSymbol, () -> {
560+
writer.write("...response,");
561+
writer.write("name: $S,", error.getId().getName());
562+
writer.write("$$fault: $S,", errorTrait.isClientError() ? "client" : "server");
563+
writer.write("$$metadata: {},");
564+
});
565+
writer.write("throw error;");
566+
});
567+
});
568+
writeServerResponseTest(operation, testCase);
569+
});
570+
}
571+
572+
private void writeServerResponseTest(OperationShape operation, HttpResponseTestCase testCase) {
573+
Symbol serviceSymbol = serverSymbolProvider.toSymbol(service);
574+
Symbol operationSymbol = serverSymbolProvider.toSymbol(operation);
575+
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
576+
Symbol serializerSymbol = operationSymbol.expectProperty("serializerType", Symbol.class);
577+
Symbol serviceOperationsSymbol = serviceSymbol.expectProperty("operations", Symbol.class);
578+
writer.write("const service: any = new TestService()");
579+
580+
// There's a lot of setup here, including creating our own mux, serializers list, and ultimately
581+
// our own service handler. This is largely in service of avoiding having to go through the
582+
// request deserializer
583+
writer.addImport("httpbinding", null, "@aws-smithy/server-common");
584+
writer.openBlock("const testMux = new httpbinding.HttpBindingMux<$S, keyof $T>([", "]);",
585+
service.getId().getName(), serviceSymbol, () -> {
586+
writer.openBlock("new httpbinding.UriSpec<$S, $S>('POST', [], [], {", "}),",
587+
service.getId().getName(), operation.getId().getName(), () -> {
588+
writer.write("service: $S,", service.getId().getName());
589+
writer.write("operation: $S,", operation.getId().getName());
590+
});
591+
});
592+
593+
// Extend the existing serializer and replace the deserialize with a noop so we don't have to
594+
// worry about trying to construct something that matches.
595+
writer.openBlock("class TestSerializer extends $T {", "}", serializerSymbol, () -> {
596+
writer.openBlock("deserialize = (output: any, context: any): Promise<any> => {", "};", () -> {
597+
writer.write("return Promise.resolve({});");
598+
});
599+
});
600+
601+
// Since we aren't going through the deserializer, we don't have to put much in the fake request.
602+
// Just enough to get it through our test mux.
603+
writer.write("const request = new HttpRequest({method: 'POST', hostname: 'example.com'});");
604+
605+
// Create a new serializer factory that always returns our test serializer.
606+
writer.addImport("SmithyException", "__SmithyException", "@aws-sdk/smithy-client");
607+
writer.addImport("OperationSerializer", "__OperationSerializer", "@aws-smithy/server-common");
608+
writer.openBlock("const serFn: (op: $1T) => __OperationSerializer<$2T, $1T, __SmithyException> = (op) =>"
609+
+ " { return new TestSerializer(); };", serviceOperationsSymbol, serviceSymbol);
610+
611+
writer.write("const handler = new $T(service, testMux, serFn);", handlerSymbol);
612+
writer.write("let r = await handler.handle(request)").write("");
613+
writeHttpResponseAssertions(testCase);
614+
}
615+
557616
private void generateErrorResponseTest(
558617
OperationShape operation,
559618
StructureShape error,

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,16 @@ private void writeErrorHandler() {
183183
writer.addImport("SerdeContext", null, "@aws-sdk/types");
184184
writer.openBlock("serializeError(error: $T, ctx: Omit<SerdeContext, 'endpoint'>): Promise<$T> {", "}",
185185
errorsType, applicationProtocol.getResponseType(), () -> {
186-
writer.openBlock("switch (error.name) {", "}", () -> {
187-
for (ShapeId errorId : operation.getErrors()) {
188-
writeErrorHandlerCase(errorId);
189-
}
190-
writer.openBlock("default: {", "}", () -> writer.write("throw error;"));
191-
});
186+
if (operation.getErrors().isEmpty()) {
187+
writer.write("throw error;");
188+
} else {
189+
writer.openBlock("switch (error.name) {", "}", () -> {
190+
for (ShapeId errorId : operation.getErrors()) {
191+
writeErrorHandlerCase(errorId);
192+
}
193+
writer.openBlock("default: {", "}", () -> writer.write("throw error;"));
194+
});
195+
}
192196
});
193197
writer.write("");
194198
}

0 commit comments

Comments
 (0)