Put
parent
c9eeedc6f6
commit
56481c420e
@ -45,7 +45,8 @@ void cashCardListDeserializationTest() throws IOException {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# GET list
|
# GET list
|
||||||
|
|
||||||
## DB data
|
## DB data
|
||||||
|
|
||||||
It is important to populate our test db with known data, that would reflect desired test case scenario. `src/test/resource/data.sql`
|
It is important to populate our test db with known data, that would reflect desired test case scenario. `src/test/resource/data.sql`
|
||||||
|
6
Post.md
6
Post.md
@ -113,4 +113,8 @@ This is constructing a URI to the newly created `CashCard`. This is the URI that
|
|||||||
|
|
||||||
> Where did `UriComponentsBuilder` come from?
|
> Where did `UriComponentsBuilder` come from?
|
||||||
|
|
||||||
We were able to add `UriComponentsBuilder ucb` as a method argument to this `POST` handler method and it was automatically passed in. How so? It was injected from our now-familiar friend, Spring's IoC Container. Thanks, Spring Web!
|
We were able to add `UriComponentsBuilder ucb` as a method argument to this `POST` handler method and it was automatically passed in. How so? It was injected from our now-familiar friend, Spring's IoC Container. Thanks, Spring Web!
|
||||||
|
|
||||||
|
# Security
|
||||||
|
|
||||||
|
It is pretty obvious, that the system should have some way of handling data from different users. The [[Home#Security]] section is a good place to continue reading. You can continue explore other CRUD methods afterwards.
|
181
Put.md
Normal file
181
Put.md
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# Test
|
||||||
|
|
||||||
|
```java
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
...
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DirtiesContext
|
||||||
|
void shouldUpdateAnExistingCashCard() {
|
||||||
|
CashCard cashCardUpdate = new CashCard(null, 19.99, null);
|
||||||
|
HttpEntity<CashCard> request = new HttpEntity<>(cashCardUpdate);
|
||||||
|
ResponseEntity<Void> response = restTemplate
|
||||||
|
.withBasicAuth("sarah1", "abc123")
|
||||||
|
.exchange("/cashcards/99", HttpMethod.PUT, request, Void.class);
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
|
||||||
|
|
||||||
|
// Validte that the resource was updated
|
||||||
|
ResponseEntity<String> getResponse = restTemplate
|
||||||
|
.withBasicAuth("sarah1", "abc123")
|
||||||
|
.getForEntity("/cashcards/99", String.class);
|
||||||
|
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
DocumentContext documentContext = JsonPath.parse(getResponse.getBody());
|
||||||
|
Number id = documentContext.read("$.id");
|
||||||
|
Double amount = documentContext.read("$.amount");
|
||||||
|
assertThat(id).isEqualTo(99);
|
||||||
|
assertThat(amount).isEqualTo(19.99);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `restTemplate.exchange()`
|
||||||
|
|
||||||
|
All other tests use `RestTemplate.xyzForEntity()` methods such as `getForEntity()` and `postForEntity()`. So, why are we not following the same pattern utilizing `putForEntity()`?
|
||||||
|
|
||||||
|
Answer: `putForEntity()` does not exist! Read more about it here in the [GitHub issue](https://github.com/spring-projects/spring-framework/issues/15256) about the topic.
|
||||||
|
|
||||||
|
Luckily `RestTemplate` supports multiple ways of interacting with REST APIs, such as `RestTemplate.exchange()`. The `exchange()` method is a more general version of the `xyzForEntity()` methods we've used in other tests: `exchange()` requires the verb and the request entity (the body of the request) to be supplied as parameters. The two lines below are functionally equivalent of one another:
|
||||||
|
|
||||||
|
-
|
||||||
|
```java
|
||||||
|
.exchange("/cashcards/99", HttpMethod.GET, new HttpEntity(null), String.class);
|
||||||
|
```
|
||||||
|
|
||||||
|
-
|
||||||
|
```java
|
||||||
|
`.getForEntity("/cashcards/99", String.class);`
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's explain the test code.
|
||||||
|
|
||||||
|
- First we create the `HttpEntity` that the `exchange()` method needs:
|
||||||
|
|
||||||
|
```java
|
||||||
|
HttpEntity<CashCard> request = new HttpEntity<>(existingCashCard);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then we call `exchange()`, which sends a `PUT` request for the target ID of `99` and updated Cash Card data:
|
||||||
|
|
||||||
|
```java
|
||||||
|
.exchange("/cashcards/99", HttpMethod.PUT, request, Void.class);
|
||||||
|
```
|
||||||
|
|
||||||
|
- And finally, we are using `.getForEntity()` to ensure that the requested resource was updated.
|
||||||
|
|
||||||
|
Run the test.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
[~/exercises] $ ./gradlew test
|
||||||
|
...
|
||||||
|
CashCardApplicationTests > shouldUpdateAnExistingCashCard() FAILED
|
||||||
|
org.opentest4j.AssertionFailedError:
|
||||||
|
expected: 204 NO_CONTENT
|
||||||
|
but was: 403 FORBIDDEN
|
||||||
|
...
|
||||||
|
BUILD FAILED in 6s
|
||||||
|
```
|
||||||
|
|
||||||
|
It has failed with a `403 FORBIDDEN` response code, because we haven't implemented a `PUT` request method handler yet. With no Controller endpoint, this `PUT` call is forbidden! Spring Security automatically handled this scenario for us.
|
||||||
|
|
||||||
|
# Implement @PutMapping in the Controller
|
||||||
|
|
||||||
|
```java
|
||||||
|
@PutMapping("/{requestedId}")
|
||||||
|
private ResponseEntity<Void> putCashCard(@PathVariable Long requestedId, @RequestBody CashCard cashCardUpdate, Principal principal) {
|
||||||
|
CashCard cashCard = cashCardRepository.findByIdAndOwner(requestedId, principal.getName());
|
||||||
|
CashCard updatedCashCard = new CashCard(cashCard.id(), cashCardUpdate.amount(), principal.getName());
|
||||||
|
cashCardRepository.save(updatedCashCard);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This Controller endpoint is fairly self-explanatory:
|
||||||
|
|
||||||
|
- The `@PutMapping` supports the `PUT` verb and supplies the target `requestedId`.
|
||||||
|
- The `@RequestBody` contains the updated `CashCard` data.
|
||||||
|
- We've added the `Principal` as a method argument, provided automatically by Spring Security. Thanks once again, Spring Security!
|
||||||
|
- Than we scope our retrieval of the `CashCard` to the submitted `requestedId` and `Principal` (provided by Spring Security) to ensure only the authenticated, authorized `owner` may update this `CashCard`
|
||||||
|
|
||||||
|
```java
|
||||||
|
cashCardRepository.findByIdAndOwner(requestedId, principal.getName());
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build a `CashCard` with updated values and save it.
|
||||||
|
- Return an HTTP `204 NO_CONTENT` response code for now just to get started.
|
||||||
|
|
||||||
|
But what happens if we attempt to update a `CashCard` that does not exist?
|
||||||
|
|
||||||
|
# Update non existing resource?
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void shouldNotUpdateACashCardThatDoesNotExist() {
|
||||||
|
CashCard unknownCard = new CashCard(null, 19.99, null);
|
||||||
|
HttpEntity<CashCard> request = new HttpEntity<>(unknownCard);
|
||||||
|
ResponseEntity<Void> response = restTemplate
|
||||||
|
.withBasicAuth("sarah1", "abc123")
|
||||||
|
.exchange("/cashcards/99999", HttpMethod.PUT, request, Void.class);
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the tests and observe the results.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
...
|
||||||
|
CashCardApplicationTests > shouldNotUpdateACashCardThatDoesNotExist() FAILED
|
||||||
|
org.opentest4j.AssertionFailedError:
|
||||||
|
expected: 404 NOT_FOUND
|
||||||
|
but was: 403 FORBIDDEN
|
||||||
|
```
|
||||||
|
|
||||||
|
Before we run them again, let's edit `build.gradle` to enable additional test output.
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
test {
|
||||||
|
testLogging {
|
||||||
|
...
|
||||||
|
// Change to `true` for more verbose test output
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After rerunning the tests, search the output for the following:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
CashCardApplicationTests > shouldNotUpdateACashCardThatDoesNotExist() STANDARD_OUT
|
||||||
|
...
|
||||||
|
java.lang.NullPointerException: Cannot invoke "example.cashcard.CashCard.id()" because "cashCard" is null
|
||||||
|
```
|
||||||
|
|
||||||
|
> A `NullPointerException`! But why?
|
||||||
|
|
||||||
|
Looking at `CashCardController.putCashCard` we can see that if we don't find the `cashCard` then method calls to `cashCard` will result in a `NullPointerException`. That makes sense.
|
||||||
|
|
||||||
|
But why is a `NullPointerException` thrown in our Controller resulting in a `403 FORBIDDEN` instead of a `500 INTERNAL_SERVER_ERROR`, given the server "crashed?"
|
||||||
|
|
||||||
|
## SpringSecurity and HTTP error codes
|
||||||
|
|
||||||
|
Our Controller is returning `403 FORBIDDEN` instead of an `500 INTERNAL_SERVER_ERROR` because Spring Security is automatically implementing a best practice regarding how errors are handled by Spring Web.
|
||||||
|
|
||||||
|
It's important to understand that any information returned from our application might be useful to a bad actor attempting to violate our application's security. For example: knowledge about actions that causes our application to crash -- a `500 INTERNAL_SERVER_ERROR`.
|
||||||
|
|
||||||
|
In order to avoid "leaking" information about our application, Spring Security has configured Spring Web to return a generic `403 FORBIDDEN` in most error conditions. If almost everything results in a `403 FORBIDDEN` response then an attacker doesn't really know what's going on.
|
||||||
|
|
||||||
|
## Don't crash
|
||||||
|
|
||||||
|
Though we're thankful to Spring Security, our application should not crash - we shouldn't allow our code to throw a `NullPointerException`. Instead, we should handle the condition when `cashCard == null`, and return a generic `404 NOT_FOUND` HTTP response. Update the `Put` endpoint:
|
||||||
|
|
||||||
|
```java
|
||||||
|
...
|
||||||
|
CashCard cashCard = cashCardRepository.findByIdAndOwner(requestedId, principal.getName());
|
||||||
|
|
||||||
|
if (cashCard != null) { // <<-- do not crash :)
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user