diff --git a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/controllers/ServerController.java b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/controllers/ServerController.java index d465953..82e8c05 100644 --- a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/controllers/ServerController.java +++ b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/controllers/ServerController.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; @@ -63,8 +64,8 @@ public class ServerController { private final SchemaServerProperties schemaServerProperties; public ServerController(SchemaRepository repository, - Map validators, - SchemaServerProperties schemaServerProperties) { + Map validators, + SchemaServerProperties schemaServerProperties) { Assert.notNull(repository, "cannot be null"); Assert.notEmpty(validators, "cannot be empty"); this.repository = repository; @@ -73,8 +74,8 @@ public class ServerController { } @RequestMapping(method = RequestMethod.POST, path = "/", consumes = "application/json", produces = "application/json") - public synchronized ResponseEntity register(@RequestBody Schema schema, - UriComponentsBuilder builder) { + public synchronized ResponseEntity register(@RequestBody Schema schema, UriComponentsBuilder builder) { + SchemaValidator validator = this.validators.get(schema.getFormat()); if (validator == null) { @@ -83,9 +84,7 @@ public class ServerController { .collectionToCommaDelimitedString(this.validators.keySet()))); } - if (!validator.isValid(schema.getDefinition())) { - throw new InvalidSchemaException("Invalid schema"); - } + validator.validate(schema.getDefinition()); Schema result; List registeredEntities = this.repository @@ -121,12 +120,13 @@ public class ServerController { @RequestMapping(method = RequestMethod.GET, produces = "application/json", path = "/{subject}/{format}/v{version}") public ResponseEntity findOne(@PathVariable("subject") String subject, - @PathVariable("format") String format, - @PathVariable("version") Integer version) { - Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, - format, version); + @PathVariable("format") String format, + @PathVariable("version") Integer version) { + Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, format, version); if (schema == null) { - throw new SchemaNotFoundException("Could not find Schema"); + throw new SchemaNotFoundException( + String.format("Could not find Schema by subject: %s, format: %s, version %s", + subject, format, version)); } return new ResponseEntity<>(schema, HttpStatus.OK); } @@ -135,7 +135,7 @@ public class ServerController { public ResponseEntity findOne(@PathVariable("id") Integer id) { Optional schema = this.repository.findById(id); if (!schema.isPresent()) { - throw new SchemaNotFoundException("Could not find Schema"); + throw new SchemaNotFoundException(String.format("Could not find Schema by id: %s", id)); } return new ResponseEntity<>(schema.get(), HttpStatus.OK); } @@ -157,7 +157,7 @@ public class ServerController { */ @Deprecated public ResponseEntity> findBySubjectAndVersion(@PathVariable("subject") String subject, - @PathVariable("format") String format) { + @PathVariable("format") String format) { return findBySubjectAndFormatOrderByVersionAsc(subject, format); } @@ -177,21 +177,26 @@ public class ServerController { @GetMapping(produces = APPLICATION_JSON_VALUE, path = "/{subject}/{format}") @NonNull public ResponseEntity> findBySubjectAndFormat(@NonNull @PathVariable("subject") final String subject, - @NonNull @PathVariable("format") final String format) { + @NonNull @PathVariable("format") final String format) { return findBySubjectAndFormatOrderByVersionAsc(subject, format); } @RequestMapping(value = "/{subject}/{format}/v{version}", method = RequestMethod.DELETE) public void delete(@PathVariable("subject") String subject, - @PathVariable("format") String format, - @PathVariable("version") Integer version) { + @PathVariable("format") String format, + @PathVariable("version") Integer version) { if (this.schemaServerProperties.isAllowSchemaDeletion()) { - Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, - format, version); + Schema schema = this.repository.findOneBySubjectAndFormatAndVersion(subject, format, version); + if (schema == null) { + throw new SchemaNotFoundException( + String.format("Could not find Schema by subject: %s, format: %s, version %s", + subject, format, version)); + } deleteSchema(schema); } else { - throw new SchemaDeletionNotAllowedException(); + throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by " + + "subject: %s, format: %s, version %s", subject, format, version)); } } @@ -200,12 +205,12 @@ public class ServerController { if (this.schemaServerProperties.isAllowSchemaDeletion()) { Optional schema = this.repository.findById(id); if (!schema.isPresent()) { - throw new SchemaNotFoundException("Could not find Schema"); + throw new SchemaNotFoundException(String.format("Could not find Schema by id: %s", id)); } deleteSchema(schema.get()); } else { - throw new SchemaDeletionNotAllowedException(); + throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by id: %s", id)); } } @@ -219,14 +224,15 @@ public class ServerController { } } else { - throw new SchemaDeletionNotAllowedException(); + throw new SchemaDeletionNotAllowedException(String.format("Not permitted deletion of Schema by " + + "subject: %s", subject)); } } @NonNull public final ResponseEntity> findBySubjectAndFormatOrderByVersionAsc(@NonNull final String subject, - @NonNull final String format) { + @NonNull final String format) { List schemas = this.repository.findBySubjectAndFormatOrderByVersion(subject, format); if (schemas.isEmpty()) { throw new SchemaNotFoundException( @@ -243,23 +249,34 @@ public class ServerController { } @ExceptionHandler(UnsupportedFormatException.class) - @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Format not supported") - public void unsupportedFormat(UnsupportedFormatException ex) { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public String onUnsupportedFormat(UnsupportedFormatException e) { + return errorMessage("Format not supported", e); } @ExceptionHandler(InvalidSchemaException.class) - @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Invalid schema") - public void invalidSchema(InvalidSchemaException ex) { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public String onInvalidSchema(InvalidSchemaException e) { + return errorMessage("Invalid Schema", e); } @ExceptionHandler(SchemaNotFoundException.class) - @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Schema not found") - public void schemaNotFound(SchemaNotFoundException ex) { + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public String schemaNotFound(SchemaNotFoundException ex) { + return errorMessage("Schema not found", ex); } @ExceptionHandler(SchemaDeletionNotAllowedException.class) - @ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED, reason = "Schema deletion is not permitted") - public void schemaDeletionNotPermitted(SchemaDeletionNotAllowedException ex) { + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + @ResponseBody + public String schemaDeletionNotPermitted(SchemaDeletionNotAllowedException ex) { + return errorMessage("Schema deletion is not permitted", ex); } + private String errorMessage(String prefix, Throwable e) { + return prefix + (StringUtils.hasText(e.getMessage()) ? ": " + e.getMessage() : ""); + } } diff --git a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/AvroSchemaValidator.java b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/AvroSchemaValidator.java index 187746d..18f7267 100644 --- a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/AvroSchemaValidator.java +++ b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/AvroSchemaValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 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. @@ -25,9 +25,15 @@ import org.springframework.cloud.schema.registry.model.Schema; /** * @author Vinicius Carvalho + * @author Christian Tzolov */ public class AvroSchemaValidator implements SchemaValidator { + /** + * Unique Avro schema format identifier. + */ + public static final String AVRO_FORMAT = "avro"; + @Override public boolean isValid(String definition) { boolean result = true; @@ -40,6 +46,16 @@ public class AvroSchemaValidator implements SchemaValidator { return result; } + @Override + public void validate(String definition) { + try { + new org.apache.avro.Schema.Parser().parse(definition); + } + catch (SchemaParseException ex) { + throw new InvalidSchemaException((ex.getMessage())); + } + } + @Override public Compatibility compatibilityCheck(String source, String other) { return null; @@ -48,11 +64,9 @@ public class AvroSchemaValidator implements SchemaValidator { @Override public Schema match(List schemas, String definition) { Schema result = null; - org.apache.avro.Schema source = new org.apache.avro.Schema.Parser() - .parse(definition); + org.apache.avro.Schema source = new org.apache.avro.Schema.Parser().parse(definition); for (Schema s : schemas) { - org.apache.avro.Schema target = new org.apache.avro.Schema.Parser() - .parse(s.getDefinition()); + org.apache.avro.Schema target = new org.apache.avro.Schema.Parser().parse(s.getDefinition()); if (target.equals(source)) { result = s; break; @@ -63,7 +77,7 @@ public class AvroSchemaValidator implements SchemaValidator { @Override public String getFormat() { - return "avro"; + return AVRO_FORMAT; } } diff --git a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/SchemaValidator.java b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/SchemaValidator.java index 5c46518..d53b540 100644 --- a/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/SchemaValidator.java +++ b/spring-cloud-schema-registry-core/src/main/java/org/springframework/cloud/schema/registry/support/SchemaValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 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. @@ -23,6 +23,7 @@ import org.springframework.cloud.schema.registry.model.Schema; /** * @author Vinicius Carvalho + * @author Christian Tzolov * * Provides utility methods to validate, check compatibility and match schemas of * different implementations @@ -36,6 +37,17 @@ public interface SchemaValidator { */ boolean isValid(String definition); + /** + * Validates a schema definition and throws an {@link InvalidSchemaException} when the schema is invalid. + * The exception is expected to have the violation description. + * @param definition - The textual representation of the schema file + */ + default void validate(String definition) { + if (!this.isValid(definition)) { + throw new InvalidSchemaException("Invalid Schema"); + } + } + /** * Checks for compatibility between two schemas @see Compatibility class for types * This method may not be supported for certain formats. diff --git a/spring-cloud-schema-registry-server/src/test/java/org/springframework/cloud/schema/registry/server/SchemaRegistryServerAvroTests.java b/spring-cloud-schema-registry-server/src/test/java/org/springframework/cloud/schema/registry/server/SchemaRegistryServerAvroTests.java index 9d1ff9a..59de67b 100644 --- a/spring-cloud-schema-registry-server/src/test/java/org/springframework/cloud/schema/registry/server/SchemaRegistryServerAvroTests.java +++ b/spring-cloud-schema-registry-server/src/test/java/org/springframework/cloud/schema/registry/server/SchemaRegistryServerAvroTests.java @@ -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 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 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 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 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 registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess( AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1); @@ -260,14 +266,18 @@ public class SchemaRegistryServerAvroTests { assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.OK); - ResponseEntity 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 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 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 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 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 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 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 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 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 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>>> 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 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 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 registerSchemaAndAssertSuccess = registerSchemaAndAssertSuccess( AVRO_USER_REGISTRY_SCHEMA_V1, 1, 1); Schema schema = registerSchemaAndAssertSuccess.getBody(); - ResponseEntity 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> findBySubjectAndVersionResponseEntity = this.serverController .findBySubjectAndVersion(subject, format); @@ -445,10 +467,8 @@ public class SchemaRegistryServerAvroTests { final List schemaResponseBody = findBySubjectAndVersionResponseEntity.getBody(); - assertThat(schemaResponseBody) - .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 schemaResponseBody = findBySubjectFormatResponse.getBody(); - assertThat(schemaResponseBody) - .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 registerSchemaAndAssertSuccess(@NonNull Schema schema, - @Nullable Integer expectedVersion, - @Nullable Integer expectedId) { + @Nullable Integer expectedVersion, + @Nullable Integer expectedId) { ResponseEntity 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); + } + } } diff --git a/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v1.json b/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v1.json new file mode 100644 index 0000000..a6c9172 --- /dev/null +++ b/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v1.json @@ -0,0 +1,18 @@ +{ + "namespace": "example.avro", + "type": "record", + "name": "User", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "favorite_number", + "type": [ + "int", + "null" + ] + } + ] +} diff --git a/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v2.json b/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v2.json new file mode 100644 index 0000000..f251134 --- /dev/null +++ b/spring-cloud-schema-registry-server/src/test/resources/avro_user_definition_schema_v2.json @@ -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" + ] + } + ] +} diff --git a/spring-cloud-schema-registry-server/src/test/resources/invalid_schema.json b/spring-cloud-schema-registry-server/src/test/resources/invalid_schema.json new file mode 100644 index 0000000..cb1dceb --- /dev/null +++ b/spring-cloud-schema-registry-server/src/test/resources/invalid_schema.json @@ -0,0 +1,11 @@ +{ + "type": "record", + "name": "SuperType", + "namespace": "some.namespace", + "fields": [ + { + "name": "field", + "type": "SomeType" + } + ] +}