1111
1212import jakarta .servlet .http .HttpServletRequest ;
1313import java .time .Instant ;
14- import java .util . Objects ;
14+ import java .net . URI ;
1515
1616import static org .assertj .core .api .Assertions .assertThat ;
1717import 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