Skip to content

Commit 5f47b61

Browse files
committed
Finishing Task 10
1 parent d93e0ef commit 5f47b61

File tree

2 files changed

+373
-26
lines changed

2 files changed

+373
-26
lines changed

examples/spring-boot-demo/implementation/src/test/java/info/jab/ms/controller/GlobalExceptionHandlerTest.java

Lines changed: 352 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import jakarta.servlet.http.HttpServletRequest;
1313
import java.time.Instant;
14-
import java.util.Objects;
14+
import java.net.URI;
1515

1616
import static org.assertj.core.api.Assertions.assertThat;
1717
import static org.mockito.Mockito.when;
@@ -210,4 +210,355 @@ void shouldReturnConsistentErrorResponseStructure() {
210210
assertThat(problemDetail.getProperties()).containsKey("timestamp");
211211
assertThat(problemDetail.getProperties()).containsKey("errorId");
212212
}
213+
214+
// ========================================================================
215+
// Task 9.2: Test HTTP status code mapping
216+
// ========================================================================
217+
218+
@Test
219+
void shouldMapRuntimeExceptionToHttp500StatusCode() {
220+
// Given: A RuntimeException
221+
RuntimeException exception = new RuntimeException("Service unavailable");
222+
223+
// When: The exception handler processes the exception
224+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
225+
226+
// Then: Should map to HTTP 500 Internal Server Error
227+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
228+
assertThat(response.getStatusCode().value()).isEqualTo(500);
229+
230+
// And: ProblemDetail status should match ResponseEntity status
231+
ProblemDetail problemDetail = response.getBody();
232+
assertThat(problemDetail).isNotNull();
233+
assertThat(problemDetail.getStatus()).isEqualTo(500);
234+
}
235+
236+
@Test
237+
void shouldMapGenericExceptionToHttp500StatusCode() {
238+
// Given: A generic Exception
239+
Exception exception = new Exception("Unexpected failure");
240+
241+
// When: The exception handler processes the exception
242+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleGenericException(exception, mockRequest);
243+
244+
// Then: Should map to HTTP 500 Internal Server Error
245+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
246+
assertThat(response.getStatusCode().value()).isEqualTo(500);
247+
248+
// And: ProblemDetail status should match ResponseEntity status
249+
ProblemDetail problemDetail = response.getBody();
250+
assertThat(problemDetail).isNotNull();
251+
assertThat(problemDetail.getStatus()).isEqualTo(500);
252+
}
253+
254+
@Test
255+
void shouldReturnConsistentStatusCodeMappingAcrossExceptionTypes() {
256+
// Given: Different exception types that should map to the same status code
257+
RuntimeException runtimeException = new RuntimeException("Runtime error");
258+
Exception genericException = new Exception("Generic error");
259+
260+
// When: Both exceptions are processed
261+
ResponseEntity<ProblemDetail> runtimeResponse = globalExceptionHandler.handleRuntimeException(runtimeException, mockRequest);
262+
ResponseEntity<ProblemDetail> genericResponse = globalExceptionHandler.handleGenericException(genericException, mockRequest);
263+
264+
// Then: Both should map to the same HTTP status code
265+
assertThat(runtimeResponse.getStatusCode()).isEqualTo(genericResponse.getStatusCode());
266+
assertThat(runtimeResponse.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
267+
268+
// And: ProblemDetail status should be consistent
269+
assertThat(runtimeResponse.getBody().getStatus()).isEqualTo(genericResponse.getBody().getStatus());
270+
assertThat(runtimeResponse.getBody().getStatus()).isEqualTo(500);
271+
}
272+
273+
@Test
274+
void shouldValidateStatusCodeIsNotNull() {
275+
// Given: A RuntimeException
276+
RuntimeException exception = new RuntimeException("Test error");
277+
278+
// When: The exception handler processes the exception
279+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
280+
281+
// Then: Status code should never be null
282+
assertThat(response.getStatusCode()).isNotNull();
283+
assertThat(response.getBody().getStatus()).isNotNull();
284+
285+
// And: Status code should be a valid HTTP status code
286+
int statusCode = response.getStatusCode().value();
287+
assertThat(statusCode).isBetween(100, 599);
288+
}
289+
290+
@Test
291+
void shouldEnsureStatusCodeConsistencyBetweenResponseEntityAndProblemDetail() {
292+
// Given: Different types of exceptions
293+
RuntimeException runtimeException = new RuntimeException("Runtime error");
294+
Exception genericException = new Exception("Generic error");
295+
296+
// When: Both exceptions are processed
297+
ResponseEntity<ProblemDetail> runtimeResponse = globalExceptionHandler.handleRuntimeException(runtimeException, mockRequest);
298+
ResponseEntity<ProblemDetail> genericResponse = globalExceptionHandler.handleGenericException(genericException, mockRequest);
299+
300+
// Then: ResponseEntity status code should match ProblemDetail status
301+
assertThat(runtimeResponse.getStatusCode().value()).isEqualTo(runtimeResponse.getBody().getStatus());
302+
assertThat(genericResponse.getStatusCode().value()).isEqualTo(genericResponse.getBody().getStatus());
303+
304+
// And: Both should be HTTP 500
305+
assertThat(runtimeResponse.getStatusCode().value()).isEqualTo(500);
306+
assertThat(genericResponse.getStatusCode().value()).isEqualTo(500);
307+
}
308+
309+
@Test
310+
void shouldMapToAppropriateServerErrorStatusRange() {
311+
// Given: Server-side exceptions
312+
RuntimeException runtimeException = new RuntimeException("Server error");
313+
Exception genericException = new Exception("Server failure");
314+
315+
// When: Both exceptions are processed
316+
ResponseEntity<ProblemDetail> runtimeResponse = globalExceptionHandler.handleRuntimeException(runtimeException, mockRequest);
317+
ResponseEntity<ProblemDetail> genericResponse = globalExceptionHandler.handleGenericException(genericException, mockRequest);
318+
319+
// Then: Should map to 5xx server error status range
320+
int runtimeStatusCode = runtimeResponse.getStatusCode().value();
321+
int genericStatusCode = genericResponse.getStatusCode().value();
322+
323+
assertThat(runtimeStatusCode).isBetween(500, 599);
324+
assertThat(genericStatusCode).isBetween(500, 599);
325+
326+
// And: Should not map to client error 4xx range
327+
assertThat(runtimeStatusCode).isGreaterThanOrEqualTo(500);
328+
assertThat(genericStatusCode).isGreaterThanOrEqualTo(500);
329+
}
330+
331+
// ========================================================================
332+
// Task 9.3: Test error response JSON structure
333+
// ========================================================================
334+
335+
@Test
336+
void shouldReturnRfc7807CompliantJsonStructure() {
337+
// Given: A RuntimeException
338+
RuntimeException exception = new RuntimeException("Test error");
339+
340+
// When: The exception handler processes the exception
341+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
342+
343+
// Then: Should return RFC 7807 compliant JSON structure
344+
ProblemDetail problemDetail = response.getBody();
345+
assertThat(problemDetail).isNotNull();
346+
347+
// RFC 7807 mandatory fields
348+
assertThat(problemDetail.getType()).isNotNull();
349+
assertThat(problemDetail.getTitle()).isNotNull();
350+
assertThat(problemDetail.getStatus()).isNotNull();
351+
assertThat(problemDetail.getDetail()).isNotNull();
352+
assertThat(problemDetail.getInstance()).isNotNull();
353+
354+
// Field value validation
355+
assertThat(problemDetail.getType().toString()).isEqualTo("https://example.com/problems/internal-error");
356+
assertThat(problemDetail.getTitle()).isEqualTo("Internal Server Error");
357+
assertThat(problemDetail.getStatus()).isEqualTo(500);
358+
assertThat(problemDetail.getDetail()).isEqualTo("An unexpected error occurred while processing the request");
359+
assertThat(problemDetail.getInstance().toString()).isEqualTo("/api/v1/films");
360+
}
361+
362+
@Test
363+
void shouldIncludeCustomPropertiesInJsonStructure() {
364+
// Given: A RuntimeException
365+
RuntimeException exception = new RuntimeException("Test error");
366+
367+
// When: The exception handler processes the exception
368+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
369+
370+
// Then: Should include custom properties in JSON structure
371+
ProblemDetail problemDetail = response.getBody();
372+
assertThat(problemDetail).isNotNull();
373+
assertThat(problemDetail.getProperties()).isNotNull();
374+
375+
// Custom properties validation
376+
assertThat(problemDetail.getProperties()).containsKey("timestamp");
377+
assertThat(problemDetail.getProperties()).containsKey("errorId");
378+
379+
// Timestamp validation
380+
Object timestamp = problemDetail.getProperties().get("timestamp");
381+
assertThat(timestamp).isInstanceOf(Instant.class);
382+
assertThat(timestamp).isNotNull();
383+
384+
// Error ID validation
385+
Object errorId = problemDetail.getProperties().get("errorId");
386+
assertThat(errorId).isInstanceOf(String.class);
387+
assertThat(errorId.toString()).matches("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); // UUID format
388+
assertThat(errorId.toString()).isNotEmpty();
389+
}
390+
391+
@Test
392+
void shouldReturnValidJsonFieldTypes() {
393+
// Given: A RuntimeException
394+
RuntimeException exception = new RuntimeException("Test error");
395+
396+
// When: The exception handler processes the exception
397+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
398+
399+
// Then: Should return valid JSON field types
400+
ProblemDetail problemDetail = response.getBody();
401+
assertThat(problemDetail).isNotNull();
402+
403+
// Type field should be URI
404+
assertThat(problemDetail.getType()).isInstanceOf(URI.class);
405+
406+
// Title field should be String
407+
assertThat(problemDetail.getTitle()).isInstanceOf(String.class);
408+
409+
// Status field should be Integer
410+
assertThat(problemDetail.getStatus()).isInstanceOf(Integer.class);
411+
412+
// Detail field should be String
413+
assertThat(problemDetail.getDetail()).isInstanceOf(String.class);
414+
415+
// Instance field should be URI
416+
assertThat(problemDetail.getInstance()).isInstanceOf(URI.class);
417+
418+
// Properties should be Map
419+
assertThat(problemDetail.getProperties()).isInstanceOf(java.util.Map.class);
420+
}
421+
422+
@Test
423+
void shouldMaintainJsonStructureConsistencyAcrossExceptionTypes() {
424+
// Given: Different exception types
425+
RuntimeException runtimeException = new RuntimeException("Runtime error");
426+
Exception genericException = new Exception("Generic error");
427+
428+
// When: Both exceptions are processed
429+
ResponseEntity<ProblemDetail> runtimeResponse = globalExceptionHandler.handleRuntimeException(runtimeException, mockRequest);
430+
ResponseEntity<ProblemDetail> genericResponse = globalExceptionHandler.handleGenericException(genericException, mockRequest);
431+
432+
// Then: Should maintain consistent JSON structure across exception types
433+
ProblemDetail runtimeProblem = runtimeResponse.getBody();
434+
ProblemDetail genericProblem = genericResponse.getBody();
435+
436+
assertThat(runtimeProblem).isNotNull();
437+
assertThat(genericProblem).isNotNull();
438+
439+
// Structure consistency validation
440+
assertThat(runtimeProblem.getType()).isEqualTo(genericProblem.getType());
441+
assertThat(runtimeProblem.getTitle()).isEqualTo(genericProblem.getTitle());
442+
assertThat(runtimeProblem.getStatus()).isEqualTo(genericProblem.getStatus());
443+
assertThat(runtimeProblem.getDetail()).isEqualTo(genericProblem.getDetail());
444+
445+
// Properties consistency validation
446+
assertThat(runtimeProblem.getProperties().keySet()).isEqualTo(genericProblem.getProperties().keySet());
447+
assertThat(runtimeProblem.getProperties()).containsKeys("timestamp", "errorId");
448+
assertThat(genericProblem.getProperties()).containsKeys("timestamp", "errorId");
449+
}
450+
451+
@Test
452+
void shouldIncludeAllRequiredRfc7807Fields() {
453+
// Given: A generic Exception
454+
Exception exception = new Exception("System error");
455+
456+
// When: The exception handler processes the exception
457+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleGenericException(exception, mockRequest);
458+
459+
// Then: Should include all required RFC 7807 fields
460+
ProblemDetail problemDetail = response.getBody();
461+
assertThat(problemDetail).isNotNull();
462+
463+
// Required RFC 7807 fields validation
464+
assertThat(problemDetail.getType())
465+
.as("RFC 7807 'type' field is required")
466+
.isNotNull();
467+
468+
assertThat(problemDetail.getTitle())
469+
.as("RFC 7807 'title' field is required")
470+
.isNotNull()
471+
.isNotEmpty();
472+
473+
assertThat(problemDetail.getStatus())
474+
.as("RFC 7807 'status' field is required")
475+
.isNotNull()
476+
.isPositive();
477+
478+
assertThat(problemDetail.getDetail())
479+
.as("RFC 7807 'detail' field is required")
480+
.isNotNull()
481+
.isNotEmpty();
482+
483+
assertThat(problemDetail.getInstance())
484+
.as("RFC 7807 'instance' field is required")
485+
.isNotNull();
486+
}
487+
488+
@Test
489+
void shouldReturnValidUriFieldsInJsonStructure() {
490+
// Given: A RuntimeException
491+
RuntimeException exception = new RuntimeException("URI validation test");
492+
493+
// When: The exception handler processes the exception
494+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
495+
496+
// Then: Should return valid URI fields in JSON structure
497+
ProblemDetail problemDetail = response.getBody();
498+
assertThat(problemDetail).isNotNull();
499+
500+
// Type URI validation
501+
URI typeUri = problemDetail.getType();
502+
assertThat(typeUri).isNotNull();
503+
assertThat(typeUri.toString()).startsWith("https://");
504+
assertThat(typeUri.toString()).contains("problems");
505+
assertThat(typeUri.isAbsolute()).isTrue();
506+
507+
// Instance URI validation
508+
URI instanceUri = problemDetail.getInstance();
509+
assertThat(instanceUri).isNotNull();
510+
assertThat(instanceUri.toString()).startsWith("/");
511+
assertThat(instanceUri.getPath()).isNotNull();
512+
}
513+
514+
@Test
515+
void shouldProvideDescriptiveErrorMessageInJsonStructure() {
516+
// Given: A RuntimeException with specific message
517+
RuntimeException exception = new RuntimeException("Database connection timeout");
518+
519+
// When: The exception handler processes the exception
520+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
521+
522+
// Then: Should provide descriptive error message without exposing sensitive details
523+
ProblemDetail problemDetail = response.getBody();
524+
assertThat(problemDetail).isNotNull();
525+
526+
// Generic error message validation (should not expose internal details)
527+
assertThat(problemDetail.getDetail())
528+
.isEqualTo("An unexpected error occurred while processing the request")
529+
.doesNotContain("Database")
530+
.doesNotContain("timeout")
531+
.doesNotContain("connection");
532+
533+
// Title should be descriptive but generic
534+
assertThat(problemDetail.getTitle())
535+
.isEqualTo("Internal Server Error")
536+
.isNotEmpty();
537+
}
538+
539+
@Test
540+
void shouldGenerateValidTimestampInJsonStructure() {
541+
// Given: A RuntimeException
542+
RuntimeException exception = new RuntimeException("Timestamp test");
543+
Instant beforeRequest = Instant.now();
544+
545+
// When: The exception handler processes the exception
546+
ResponseEntity<ProblemDetail> response = globalExceptionHandler.handleRuntimeException(exception, mockRequest);
547+
Instant afterRequest = Instant.now();
548+
549+
// Then: Should generate valid timestamp in JSON structure
550+
ProblemDetail problemDetail = response.getBody();
551+
assertThat(problemDetail).isNotNull();
552+
553+
Object timestampObj = problemDetail.getProperties().get("timestamp");
554+
assertThat(timestampObj).isInstanceOf(Instant.class);
555+
556+
Instant timestamp = (Instant) timestampObj;
557+
assertThat(timestamp)
558+
.isAfterOrEqualTo(beforeRequest)
559+
.isBeforeOrEqualTo(afterRequest);
560+
561+
// Timestamp should be in ISO-8601 format when serialized
562+
assertThat(timestamp.toString()).matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z");
563+
}
213564
}

0 commit comments

Comments
 (0)