diff --git a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java index 13f71fd..b5c5ab7 100644 --- a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java +++ b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java @@ -127,4 +127,36 @@ class SmokeE2ETest { assertThat(newEntries).isNotEmpty(); assertThat(newEntries.get(0).getBusinessKey()).isEqualTo(businessKey); } + + /** + * Regression: unrouted path должен возвращать 404 not_found, не 500 + * internal_error. + * + *

До MR-fix: NoResourceFoundException попадал в generic Exception handler + * в GlobalExceptionHandler → 500. Симптомы на prod включали: + *

+ * + *

Тест проверяет API-typo путь — он независим от env (springdoc может + * быть enabled/disabled, не влияет). Проверка swagger-ui→404 при disabled + * springdoc относится к prod integration, не к unit-уровню handler'а. + */ + @Test + void unknownPath_returns404WithNotFoundCode() throws Exception { + mvc.perform(get("/api/v1/this-endpoint-does-not-exist")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)) + .andExpect(jsonPath("$.code").value("not_found")); + + // Generic non-API typo тоже должен 404'нуться (covers static resource + // fallback branch NoResourceFoundException — этот путь не SPA-routed). + mvc.perform(get("/totally-bogus-path-xyz-12345.unknown")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)) + .andExpect(jsonPath("$.code").value("not_found")); + } } diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java index 9cb6373..1c3ebc6 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/error/GlobalExceptionHandler.java @@ -11,6 +11,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.resource.NoResourceFoundException; import java.time.format.DateTimeParseException; @@ -73,6 +75,43 @@ public class GlobalExceptionHandler { return ResponseEntity.badRequest().body(body); } + /** + * Path не имеет зарегистрированного controller'а ИЛИ static resource'а. + * + *

Spring MVC throws: + *

+ * + *

До этого handler'а оба exception'а попадали в generic {@link Exception} + * catch ниже → возвращали 500 internal_error. Эффекты: + *

+ * + *

Лог уровня DEBUG (не WARN/ERROR) — это ожидаемый 404, не аномалия. + */ + @ExceptionHandler({NoResourceFoundException.class, NoHandlerFoundException.class}) + public ResponseEntity onNotFound(Exception e) { + log.debug("Resource not found: {}", e.getMessage()); + var body = ApiErrorResponse.of(404, "not_found", "Resource not found", currentTraceId()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); + } + @ExceptionHandler(Exception.class) public ResponseEntity onGeneric(Exception e) { log.error("Unhandled exception in REST API", e);