javamicronautmicronaut-rest

Unable to get MicronautTest to work with a POST against the HttpClient


I'm implementing a web API as a micronaut service. I have tests against the GET endpoints working, but cannot get the POST endpoints working correctly. I receive an error response 'BAD REQUEST'. The endpoint works correctly from command line curl or http(ie) POST requests.

The controller:

@Controller("/people")
@ExecuteOn(TaskExecutors.IO)  //Make sure the controller is executed on a non-main thread pool so it does not block
public class PersonController {
   private final DataService dataSvc;

   public PersonController(final DataService dataSvc) {
    this.dataSvc = dataSvc;
  }

  //Various GET endpoints

  @Post   //I also tried adding '(consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)'. This had no effect
  @Status(HttpStatus.CREATED)
  public Person addPerson(@Valid @NotNull final Person person) {
    final Person p = dataSvc.save(person);
    //log it!
    return p;
  }
}

I have an exception handler handling Throwable and returning 500 (INTERNAL_SERVICE_ERROR).

I also have a handler for ConstraintViolationExceptions, which returns a 412 (PRECONDITION_FAILED) response.

Test that's failing is:

@MicronautTest
public class PersonControllerClient {
  @Inject @Client("/people")
  HttpClient client;

  @Inject DataService dataSvc;
  final DataService mock = mock(DataService.class);

  //Various tests of the GET endpoints

  @Test
  @DisplayName("Add a person successfully")
  public void testAddPersonHappypath() {
    final Person.PersonBuilder pb = Person.builder().firstName("John").lastName("Doe");
    final Person in = pb.build();
    final Person expected = pb.id("1234").build();

    try {
      final HttpRequest<Person> request = HttpRequest.POST("/", in).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
      //Note - I also tried having the client '/', and the request endpoint '/people', which failed in the same manner
      //I also tried `HttpRequest.create(HttpMethod.POST, "/people").body(in)` with same result. 

      when(mock.save(in)).thenReturn(expected);

      final HttpResponse<Person> response = client.toBlocking().exchange(request, Person.class);
      assertEquals(HttpStatus.CREATED, response.getStatus());

      final Person body = response.body();
      assertNotNull(body);
      assertEquals(expected, body);
    } catch (HttpClientResponseException ex) {
      fail(String.format("Received exception (%s) - Status %d", ex.getMessage(), ex.getStatus().getCode()));
    }
  }
}

As far as I can see, this should succeed, but it is going to the fail (catch block).

I've tried stepping through the test and it doesn't seem to be making it into the PersonController code at all, so it's something with how the POST query is being constructed.

I'm assuming I'm creating the client incorrectly, but cannot see where. Any help would be appreciated.

Note - I'm moving from SpringBoot to Micronaut. Not sure if I'm trying to apply Spring concepts where they shouldn't be!

More info

I have added tests for the DELETE method, which succeed (both happy and unhappy paths). It's ONLY the POST requests that fail, with BAD REQUEST

Also, probably useful to have the Person data object

@MappedEntity
@Data
@EqualsAndHashCode
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Person {
  @Id
  @GeneratedValue
  String id;

  @NotEmpty(message="First name cannot be empty")
  String firstName;

  @NotEmpty(message="Last name cannot be empty")
  String lastName;

  int age;
}

Lombok annotations to make this 'record'ish. Not actually using Record as the validation annotations can't be applied to fields of a record (target for NotEmpty and Id annotations does not include RECORD_COMPONENT)


Solution

  • Answer from comment from @szymon-stepniak

    Within the post method declaration, make sure there is a @Body annotation on the incoming data packet

    @Controller("/people")
    // ...
    public class PersonController {
       // ...
    
      @Post
      @Status(HttpStatus.CREATED)
      public Person addPerson(@Valid @NotNull @Body final Person person) {
        //Note the `@Body` in the method definition
    
        final Person p = dataSvc.save(person);
        //log it!
        return p;
      }
    }
    

    Unsure why the endpoint would work but the test client fails without the annotation, but BOTH work if the annotation is added.