Provide meaningful HTTP error description

- Put error description into HTTP error response.
  - Refactor the tests to check the error message bodies.

  Resolves #22
This commit is contained in:
Christian Tzolov
2020-02-13 11:18:51 +01:00
parent f4ad537f70
commit 97d58d7209
7 changed files with 296 additions and 172 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2017 the original author or authors.
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,9 @@
package org.springframework.cloud.schema.registry.server;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -41,6 +43,7 @@ import org.springframework.cloud.schema.registry.controllers.ServerController;
import org.springframework.cloud.schema.registry.model.Schema;
import org.springframework.cloud.schema.registry.support.SchemaNotFoundException;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -50,16 +53,21 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.util.UriComponentsBuilder;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD;
/**
* @author Vinicius Carvalho
* @author Ilayaperumal Gopinathan
* @author Christian Tzolov
*/
@RunWith(SpringRunner.class)
// @checkstyle:off
@@ -70,24 +78,11 @@ public class SchemaRegistryServerAvroTests {
private static final String AVRO_FORMAT_NAME = "avro";
private static final String AVRO_USER_DEFINITION_SCHEMA_V1 = "{\"namespace\": \"example.avro\",\n"
+ " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + " \"fields\": [\n"
+ " {\"name\": \"name\", \"type\": \"string\"},\n"
+ " {\"name\": \"favorite_number\", \"type\": [\"int\", \"null\"]}\n"
+ " ]\n" + "}";
private static final String AVRO_USER_DEFINTITION_SCHEMA_V2 = "{\"namespace\": \"example.avro\",\n"
+ " \"type\": \"record\",\n" + " \"name\": \"User\",\n" + " \"fields\": [\n"
+ " {\"name\": \"name\", \"type\": \"string\"},\n"
+ " {\"name\": \"favorite_number\", \"type\": [\"int\", \"null\"]},\n"
+ " {\"name\": \"favorite_color\", \"type\": [\"string\", \"null\"]}\n"
+ " ]\n" + "}";
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V1 = new Parser()
.parse(AVRO_USER_DEFINITION_SCHEMA_V1);
.parse(resourceToString("classpath:/avro_user_definition_schema_v1.json"));
private static final org.apache.avro.Schema AVRO_USER_AVRO_SCHEMA_V2 = new Parser()
.parse(AVRO_USER_DEFINTITION_SCHEMA_V2);
.parse(resourceToString("classpath:/avro_user_definition_schema_v2.json"));
private static final String AVRO_USER_SCHEMA_DEFAULT_NAME_STRATEGY_SUBJECT = AVRO_USER_AVRO_SCHEMA_V1.getName()
.toLowerCase();
@@ -137,6 +132,8 @@ public class SchemaRegistryServerAvroTests {
.port(port)
.path(contextPath).build().toUri();
this.client.getRestTemplate().setErrorHandler(new DefaultResponseErrorHandler());
}
@NonNull
@@ -149,34 +146,50 @@ public class SchemaRegistryServerAvroTests {
}
@Test
public void testUnsupportedFormat() throws Exception {
Schema schema = new Schema();
schema.setFormat("spring");
schema.setSubject("boot");
ResponseEntity<Schema> response = this.client
.postForEntity(this.serverControllerUri, schema, Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
public void testUnsupportedFormat() {
Schema schema = toSchema("spring", "boot", null);
try {
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
}
catch (HttpClientErrorException.BadRequest badRequest) {
assertThat(badRequest.getMessage()).isEqualTo("400 : [Format not supported: Invalid format, supported types are: avro]");
}
}
@Test
public void testInvalidSchema() throws Exception {
Schema schema = new Schema();
schema.setFormat(AVRO_FORMAT_NAME);
schema.setSubject("boot");
schema.setDefinition("{}");
ResponseEntity<Schema> response = this.client
.postForEntity(this.serverControllerUri, schema, Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
public void testInvalidSchema() {
Schema schema = toSchema("boot", AVRO_FORMAT_NAME, "{}");
try {
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
}
catch (HttpClientErrorException.BadRequest badRequest) {
assertThat(badRequest.getMessage()).isEqualTo("400 : [Invalid Schema: No type: {}]");
}
}
@Test
public void testInvalidSchemaGh22() {
Schema schema = toSchema("boot", AVRO_FORMAT_NAME,
resourceToString("classpath:/invalid_schema.json"));
try {
this.client.postForEntity(this.serverControllerUri, schema, Schema.class);
fail("Expects: " + HttpStatus.BAD_REQUEST + " error");
}
catch (HttpClientErrorException.BadRequest badRequest) {
assertThat(badRequest.getMessage()).isEqualTo("400 : [Invalid Schema: \"SomeType\" is not a defined name. " +
"The type of the \"field\" field must be a defined name or a {\"type\": ...} expression.]");
}
}
@Test
public void testRegister1AvroSchema() {
Schema schema = new Schema();
schema.setFormat(AVRO_FORMAT_NAME);
schema.setSubject("org.springframework.cloud.stream.schema.User");
schema.setDefinition(SchemaRegistryServerAvroTests.AVRO_USER_DEFINITION_SCHEMA_V1);
Schema schema = toSchema("org.springframework.cloud.stream.schema.User", AVRO_FORMAT_NAME,
resourceToString("classpath:/avro_user_definition_schema_v1.json"));
registerSchemaAndAssertSuccess(schema, 1, 1);
}
@@ -207,43 +220,36 @@ public class SchemaRegistryServerAvroTests {
URI findByIdUriId1 = this.serverControllerUri.resolve("/schemas/" + 2);
ResponseEntity<Schema> response = this.client
.getForEntity(findByIdUriId1, Schema.class);
final HttpStatus statusCode = response.getStatusCode();
assertThat(statusCode).isEqualTo(HttpStatus.NOT_FOUND);
try {
this.client.getForEntity(findByIdUriId1, Schema.class);
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
}
catch (HttpClientErrorException.NotFound notFound) {
assertThat(notFound.getMessage()).isEqualTo("404 : [Schema not found: Could not find Schema by id: 2]");
}
}
@Test
public void testUserSchemaV2() {
registerSchemasAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1,
AVRO_USER_REGISTRY_SCHEMA_V2);
registerSchemasAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, AVRO_USER_REGISTRY_SCHEMA_V2);
}
@Test
public void testIdempotentRegistration() {
registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
}
@Test
public void testSchemaNotfound() throws Exception {
ResponseEntity<Schema> response = this.client
.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
@Test(expected = HttpClientErrorException.NotFound.class)
public void testSchemaNotfound() {
this.client.getForEntity("http://localhost:8990/foo/avro/v42", Schema.class);
}
@Test
public void testSchemaDeletionBySubjectFormatVersion() throws Exception {
public void testSchemaDeletionBySubjectFormatVersion() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
@@ -260,14 +266,18 @@ public class SchemaRegistryServerAvroTests {
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
ResponseEntity<Schema> findBySubjectFormatVersionUriResponse = this.client
.getForEntity(subjectFormatVersionUri, Schema.class);
try {
this.client.getForEntity(subjectFormatVersionUri, Schema.class);
}
catch (HttpClientErrorException.NotFound notFound) {
assertThat(notFound.getMessage()).isEqualTo("404 : [Schema not found: Could not find Schema by " +
"subject: user, format: avro, version 1]");
}
assertThat(findBySubjectFormatVersionUriResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionBySubjectFormatVersionNotFound() throws Exception {
public void testSchemaDeletionBySubjectFormatVersionNotFound() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
@@ -275,35 +285,39 @@ public class SchemaRegistryServerAvroTests {
this.schemaServerProperties.setAllowSchemaDeletion(true);
URI subjectFormatVersionUri = this.serverControllerUri
.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation().toString().replace("v1", "v100"));
.resolve(registerSchemaAndAssertSuccess.getHeaders()
.getLocation().toString().replace("v1", "v100"));
ResponseEntity<Void> deleteResponse = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, subjectFormatVersionUri),
Void.class);
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
try {
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, subjectFormatVersionUri), Void.class);
}
catch (HttpClientErrorException.NotFound notFound) {
assertThat(notFound.getMessage()).isEqualTo("404 : [Schema not found: Could not find Schema by " +
"subject: user, format: avro, version 100]");
}
}
@Test
public void testSchemaDeletionBySubjectFormatVersionNotAllowed() throws Exception {
public void testSchemaDeletionBySubjectFormatVersionNotAllowed() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
URI versionUri = this.serverControllerUri
.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation());
URI versionUri = this.serverControllerUri.resolve(registerSchemaAndAssertSuccess.getHeaders().getLocation());
ResponseEntity<Void> deleteResponse = this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, versionUri),
Void.class);
assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
try {
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, versionUri), Void.class);
}
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : [Schema deletion is not permitted: Not permitted " +
"deletion of Schema by subject: user, format: avro, version 1]");
}
}
@Test
public void testSchemaDeletionById() throws Exception {
public void testSchemaDeletionById() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
@@ -312,46 +326,57 @@ public class SchemaRegistryServerAvroTests {
this.client.delete(this.serverControllerUri
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
ResponseEntity<Schema> response3 = this.client
.getForEntity(registerSchemaAndAssertSuccess.getHeaders().getLocation(), Schema.class);
assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
try {
this.client.getForEntity(registerSchemaAndAssertSuccess.getHeaders().getLocation(), Schema.class);
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
}
catch (HttpClientErrorException.NotFound notFound) {
assertThat(notFound.getMessage()).isEqualTo("404 : [Schema not found: Could not find Schema by subject: " +
"user, format: avro, version 1]");
}
}
@Test
public void testSchemaDeletionByIdNotFound() throws Exception {
public void testSchemaDeletionByIdNotFound() {
registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
this.schemaServerProperties.setAllowSchemaDeletion(true);
ResponseEntity<Void> deleteByIdResponse = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
.resolve("/schemas/" + 2)),
Void.class);
try {
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE,
this.serverControllerUri.resolve("/schemas/" + 2)), Void.class);
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
}
catch (HttpClientErrorException.NotFound notFound) {
assertThat(notFound.getMessage()).isEqualTo("404 : [Schema not found: Could not find Schema by id: 2]");
}
assertThat(deleteByIdResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void testSchemaDeletionByIdNotAllowed() throws Exception {
public void testSchemaDeletionByIdNotAllowed() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
URI schemaIdUri = this.serverControllerUri
.resolve(this.serverControllerUri
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
URI schemaIdUri = this.serverControllerUri.resolve(this.serverControllerUri
.resolve("/schemas/" + registerSchemaAndAssertSuccess.getBody().getVersion()));
ResponseEntity<Void> exchange = this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, schemaIdUri),
Void.class);
try {
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE, schemaIdUri), Void.class);
fail("Expects: " + HttpStatus.METHOD_NOT_ALLOWED + " error");
}
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : [Schema deletion is not permitted: Not " +
"permitted deletion of Schema by id: 1]");
}
assertThat(exchange.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
@Test
public void testSchemaDeletionBySubject() {
public void testSchemaDeletionBySubject() {
Map<String, Map<String, List<ResponseEntity<Schema>>>> registerSchemaResponsesByFormatBySubject = registerSchemasAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1,
AVRO_USER_REGISTRY_SCHEMA_V2, AAVRO_USER_REGISTRY_SCHEMA_V1_WITH_QUAL_SUBJECT);
@@ -374,12 +399,13 @@ public class SchemaRegistryServerAvroTests {
registerSchemaResponses.forEach(registerSchemaResponse -> {
ResponseEntity<Schema> shouldBe404Response = this.client.getForEntity(
registerSchemaResponse.getHeaders().getLocation(),
Schema.class);
assertThat(shouldBe404Response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
try {
this.client.getForEntity(registerSchemaResponse.getHeaders().getLocation(), Schema.class);
fail("Expects: " + HttpStatus.NOT_FOUND + " error");
}
catch (HttpClientErrorException.NotFound notFound) {
//do nothing
}
});
});
});
@@ -387,39 +413,36 @@ public class SchemaRegistryServerAvroTests {
}
@Test
public void testSchemaDeletionBySubjectNotFound() throws Exception {
public void testSchemaDeletionBySubjectNotFound() {
registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
registerSchemaAndAssertSuccess(AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
this.schemaServerProperties.setAllowSchemaDeletion(true);
ResponseEntity<Void> deleteBySubject = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
.resolve("/foo")),
Void.class);
assertThat(deleteBySubject.getStatusCode())
.isEqualTo(HttpStatus.OK);
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri.resolve("/foo")), Void.class);
assertThat(deleteBySubject.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void testSchemaDeletionBySubjectNotAllowed() throws Exception {
public void testSchemaDeletionBySubjectNotAllowed() {
ResponseEntity<Schema> registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess(
AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1);
Schema schema = registerSchemaAndAssertSuccess.getBody();
ResponseEntity<Void> deleteBySubject = this.client.exchange(
new RequestEntity<>(HttpMethod.DELETE, this.serverControllerUri
.resolve("/" + schema.getSubject())),
Void.class);
assertThat(deleteBySubject.getStatusCode())
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
try {
this.client.exchange(new RequestEntity<>(HttpMethod.DELETE,
this.serverControllerUri.resolve("/" + schema.getSubject())), Void.class);
fail("Expects: " + HttpStatus.METHOD_NOT_ALLOWED + " error");
}
catch (HttpClientErrorException.MethodNotAllowed methodNotAllowed) {
assertThat(methodNotAllowed.getMessage()).isEqualTo("405 : [Schema deletion is not permitted: " +
"Not permitted deletion of Schema by subject: user]");
}
}
@@ -437,7 +460,6 @@ public class SchemaRegistryServerAvroTests {
schemasByFormat.forEach((format, schemas) -> {
assertThat(schemas).hasSize(2);
@SuppressWarnings("deprecation")
final ResponseEntity<List<Schema>> findBySubjectAndVersionResponseEntity = this.serverController
.findBySubjectAndVersion(subject, format);
@@ -445,10 +467,8 @@ public class SchemaRegistryServerAvroTests {
final List<Schema> schemaResponseBody = findBySubjectAndVersionResponseEntity.getBody();
assertThat(schemaResponseBody)
.<Schema>zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
.collect(toList()), this::assertSchema);
assertThat(schemaResponseBody).zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
.collect(toList()), this::assertSchema);
});
});
@@ -465,7 +485,6 @@ public class SchemaRegistryServerAvroTests {
.withMessage("No schemas found for subject %s and format %s", subject, format)
.withNoCause();
}
@Test
@@ -491,10 +510,8 @@ public class SchemaRegistryServerAvroTests {
final List<Schema> schemaResponseBody = findBySubjectFormatResponse.getBody();
assertThat(schemaResponseBody)
.<Schema>zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
.collect(toList()), this::assertSchema);
assertThat(schemaResponseBody).zipSatisfy(schemas.stream().map(ResponseEntity::getBody)
.collect(toList()), this::assertSchema);
});
});
@@ -538,8 +555,8 @@ public class SchemaRegistryServerAvroTests {
@NonNull
private ResponseEntity<Schema> registerSchemaAndAssertSuccess(@NonNull Schema schema,
@Nullable Integer expectedVersion,
@Nullable Integer expectedId) {
@Nullable Integer expectedVersion,
@Nullable Integer expectedId) {
ResponseEntity<Schema> registerReponse = this.client
.postForEntity(this.serverControllerUri, schema, Schema.class);
@@ -582,7 +599,7 @@ public class SchemaRegistryServerAvroTests {
}
private void assertSchema(@NonNull Schema expected, Integer expectedVersion, Integer expectedId,
@NonNull Schema actual) {
@NonNull Schema actual) {
assertThat(actual).isEqualToIgnoringGivenFields(expected, "version", "id");
if (expectedVersion != null) {
@@ -592,4 +609,14 @@ public class SchemaRegistryServerAvroTests {
assertThat(actual.getId()).isEqualTo(expectedId);
}
}
private static String resourceToString(String resourceUri) {
try {
return StreamUtils.copyToString(new DefaultResourceLoader().getResource(resourceUri)
.getInputStream(), StandardCharsets.UTF_8);
}
catch (IOException e) {
throw new IllegalStateException("Could not extract resource: " + resourceUri, e);
}
}
}

View File

@@ -0,0 +1,18 @@
{
"namespace": "example.avro",
"type": "record",
"name": "User",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "favorite_number",
"type": [
"int",
"null"
]
}
]
}

View File

@@ -0,0 +1,25 @@
{
"namespace": "example.avro",
"type": "record",
"name": "User",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "favorite_number",
"type": [
"int",
"null"
]
},
{
"name": "favorite_color",
"type": [
"string",
"null"
]
}
]
}

View File

@@ -0,0 +1,11 @@
{
"type": "record",
"name": "SuperType",
"namespace": "some.namespace",
"fields": [
{
"name": "field",
"type": "SomeType"
}
]
}