• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

felleslosninger / einnsyn-backend / 22065575986

16 Feb 2026 01:58PM UTC coverage: 84.319% (+0.6%) from 83.69%
22065575986

push

github

web-flow
Merge pull request #589 from felleslosninger/gne-error-test-improvements

EIN-X: EInnsynExceptionHandler tests

2464 of 3355 branches covered (73.44%)

Branch coverage included in aggregate %.

7516 of 8481 relevant lines covered (88.62%)

3.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.78
src/main/java/no/einnsyn/backend/error/EInnsynExceptionHandler.java
1
package no.einnsyn.backend.error;
2

3
import com.google.gson.JsonParseException;
4
import io.micrometer.core.instrument.MeterRegistry;
5
import java.util.List;
6
import java.util.stream.Collectors;
7
import lombok.extern.slf4j.Slf4j;
8
import no.einnsyn.backend.common.exceptions.models.AuthenticationException;
9
import no.einnsyn.backend.common.exceptions.models.AuthorizationException;
10
import no.einnsyn.backend.common.exceptions.models.BadRequestException;
11
import no.einnsyn.backend.common.exceptions.models.ConflictException;
12
import no.einnsyn.backend.common.exceptions.models.EInnsynException;
13
import no.einnsyn.backend.common.exceptions.models.InternalServerErrorException;
14
import no.einnsyn.backend.common.exceptions.models.MethodNotAllowedException;
15
import no.einnsyn.backend.common.exceptions.models.NotFoundException;
16
import no.einnsyn.backend.common.exceptions.models.TooManyUnverifiedOrdersException;
17
import no.einnsyn.backend.common.exceptions.models.ValidationException;
18
import no.einnsyn.backend.common.exceptions.models.ValidationException.FieldError;
19
import org.jetbrains.annotations.NotNull;
20
import org.springframework.context.MessageSourceResolvable;
21
import org.springframework.dao.DataIntegrityViolationException;
22
import org.springframework.http.HttpHeaders;
23
import org.springframework.http.HttpStatus;
24
import org.springframework.http.HttpStatusCode;
25
import org.springframework.http.ResponseEntity;
26
import org.springframework.http.converter.HttpMessageNotReadableException;
27
import org.springframework.transaction.TransactionSystemException;
28
import org.springframework.web.HttpRequestMethodNotSupportedException;
29
import org.springframework.web.bind.MethodArgumentNotValidException;
30
import org.springframework.web.bind.annotation.ControllerAdvice;
31
import org.springframework.web.bind.annotation.ExceptionHandler;
32
import org.springframework.web.context.request.ServletWebRequest;
33
import org.springframework.web.context.request.WebRequest;
34
import org.springframework.web.method.annotation.HandlerMethodValidationException;
35
import org.springframework.web.servlet.NoHandlerFoundException;
36
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
37

38
@ControllerAdvice
39
@Slf4j
4✔
40
public class EInnsynExceptionHandler extends ResponseEntityExceptionHandler {
41

42
  private static final String RESPONSE_STATUS = "responseStatus";
43
  private static final String METRIC_NAME = "ein_exception";
44
  private static final String METRIC_LEVEL_KEY = "level";
45
  private static final String METRIC_EXCEPTION_KEY = "exception";
46
  private static final String METRIC_LEVEL_WARNING = "warning";
47
  private static final String METRIC_LEVEL_ERROR = "error";
48

49
  private final MeterRegistry meterRegistry;
50

51
  public EInnsynExceptionHandler(MeterRegistry meterRegistry) {
52
    super();
2✔
53
    this.meterRegistry = meterRegistry;
3✔
54
  }
1✔
55

56
  private void logAndCountWarning(EInnsynException ex, HttpStatusCode statusCode) {
57
    var exceptionName = ex.getClass().getSimpleName();
4✔
58
    log.atWarn()
3✔
59
        .setMessage(ex.getMessage())
4✔
60
        .addKeyValue(RESPONSE_STATUS, String.valueOf(statusCode))
2✔
61
        .log();
1✔
62
    meterRegistry
21✔
63
        .counter(
1✔
64
            METRIC_NAME,
65
            METRIC_LEVEL_KEY,
66
            METRIC_LEVEL_WARNING,
67
            METRIC_EXCEPTION_KEY,
68
            exceptionName)
69
        .increment();
1✔
70
  }
1✔
71

72
  private void logAndCountError(EInnsynException ex, HttpStatusCode statusCode) {
73
    var exceptionName = ex.getClass().getSimpleName();
4✔
74
    log.atError()
3✔
75
        .setMessage(ex.getMessage())
4✔
76
        .addKeyValue(RESPONSE_STATUS, String.valueOf(statusCode))
3✔
77
        .setCause(ex)
1✔
78
        .log();
1✔
79
    meterRegistry
21✔
80
        .counter(
1✔
81
            METRIC_NAME, METRIC_LEVEL_KEY, METRIC_LEVEL_ERROR, METRIC_EXCEPTION_KEY, exceptionName)
82
        .increment();
1✔
83
  }
1✔
84

85
  @ExceptionHandler(Exception.class)
86
  public ResponseEntity<Object> handleException(Exception ex) {
87
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
2✔
88
    var internalServerErrorException =
6✔
89
        new InternalServerErrorException("Internal server error", ex);
90
    logAndCountError(internalServerErrorException, httpStatus);
4✔
91
    return new ResponseEntity<>(internalServerErrorException.toClientResponse(), null, httpStatus);
8✔
92
  }
93

94
  @ExceptionHandler(TransactionSystemException.class)
95
  public ResponseEntity<Object> handleException(TransactionSystemException ex) {
96
    if (ex.getRootCause() instanceof EInnsynException eInnsynException) {
9✔
97
      return handleException(eInnsynException);
4✔
98
    }
99

100
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
2✔
101
    var internalServerErrorException =
6✔
102
        new InternalServerErrorException("Transaction system exception", ex);
103
    logAndCountError(internalServerErrorException, httpStatus);
4✔
104
    var clientResponse = internalServerErrorException.toClientResponse();
3✔
105
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
106
  }
107

108
  /**
109
   * 400 Bad Request
110
   *
111
   * @param ex The exception
112
   * @return The response entity
113
   */
114
  @ExceptionHandler(IllegalArgumentException.class)
115
  public ResponseEntity<Object> handleException(IllegalArgumentException ex) {
116
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
117
    var badRequestException = new BadRequestException(ex.getMessage(), ex);
7✔
118
    logAndCountWarning(badRequestException, httpStatus);
4✔
119
    var clientResponse = badRequestException.toClientResponse();
3✔
120
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
121
  }
122

123
  /**
124
   * 400 Bad Request
125
   *
126
   * @param ex The exception
127
   * @return The response entity
128
   */
129
  @ExceptionHandler(BadRequestException.class)
130
  public ResponseEntity<Object> handleException(BadRequestException ex) {
131
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
132
    logAndCountWarning(ex, httpStatus);
4✔
133
    var clientResponse = ex.toClientResponse();
3✔
134
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
135
  }
136

137
  /**
138
   * 403 Forbidden
139
   *
140
   * @param ex The exception
141
   * @return The response entity
142
   */
143
  @ExceptionHandler(AuthorizationException.class)
144
  public ResponseEntity<Object> handleException(AuthorizationException ex) {
145
    var httpStatus = HttpStatus.FORBIDDEN;
2✔
146
    logAndCountWarning(ex, httpStatus);
4✔
147
    var clientResponse = ex.toClientResponse();
3✔
148
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
149
  }
150

151
  /**
152
   * 401 Unauthorized
153
   *
154
   * @param ex The exception
155
   * @return The response entity
156
   */
157
  @ExceptionHandler(AuthenticationException.class)
158
  public ResponseEntity<Object> handleException(AuthenticationException ex) {
159
    var httpStatus = HttpStatus.UNAUTHORIZED;
2✔
160
    logAndCountWarning(ex, httpStatus);
4✔
161
    var clientResponse = ex.toClientResponse();
3✔
162
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
163
  }
164

165
  /*
166
   * 404 Not Found
167
   *
168
   * @param ex The exception
169
   * @return The response entity
170
   */
171
  @ExceptionHandler(NotFoundException.class)
172
  public ResponseEntity<Object> handleException(NotFoundException ex) {
173
    var httpStatus = HttpStatus.NOT_FOUND;
2✔
174
    logAndCountWarning(ex, httpStatus);
4✔
175
    var clientResponse = ex.toClientResponse();
3✔
176
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
177
  }
178

179
  /**
180
   * DataIntegrityViolation
181
   *
182
   * <p>Likely a unique constraint violation, foreign key constraint violation, not-null constraint
183
   * or similar.
184
   *
185
   * @param ex The exception
186
   */
187
  @ExceptionHandler(DataIntegrityViolationException.class)
188
  public ResponseEntity<Object> handleDataIntegrityViolationException(Exception ex) {
189
    var httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
2✔
190
    var internalServerErrorException =
6✔
191
        new InternalServerErrorException("Data integrity violation", ex);
192
    logAndCountError(internalServerErrorException, httpStatus);
4✔
193
    var clientResponse = internalServerErrorException.toClientResponse();
3✔
194
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
195
  }
196

197
  /**
198
   * Conflict
199
   *
200
   * @param ex The exception
201
   */
202
  @ExceptionHandler(ConflictException.class)
203
  public ResponseEntity<Object> handleConflictException(ConflictException ex) {
204
    var httpStatus = HttpStatus.CONFLICT;
2✔
205
    logAndCountWarning(ex, httpStatus);
4✔
206
    var clientResponse = ex.toClientResponse();
3✔
207
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
208
  }
209

210
  /**
211
   * Too many unverified orders.
212
   *
213
   * @param ex the exception
214
   * @return the response entity
215
   */
216
  @ExceptionHandler(TooManyUnverifiedOrdersException.class)
217
  public ResponseEntity<Object> handleTooManyUnverifiedOrdersException(
218
      TooManyUnverifiedOrdersException ex) {
219
    var httpStatus = HttpStatus.TOO_MANY_REQUESTS;
2✔
220
    logAndCountWarning(ex, httpStatus);
4✔
221
    var clientResponse = ex.toClientResponse();
3✔
222
    return new ResponseEntity<>(clientResponse, null, httpStatus);
7✔
223
  }
224

225
  /**
226
   * Input validation errors, @Valid and @Validated on JSON fields.
227
   *
228
   * @param ex The exception
229
   * @param headers The headers
230
   * @param status The status
231
   * @param request The request
232
   */
233
  @Override
234
  protected ResponseEntity<Object> handleMethodArgumentNotValid(
235
      @NotNull MethodArgumentNotValidException ex,
236
      @NotNull HttpHeaders headers,
237
      @NotNull HttpStatusCode status,
238
      @NotNull WebRequest request) {
239

240
    var fieldErrors =
1✔
241
        ex.getFieldErrors().stream()
3✔
242
            .map(
1✔
243
                e ->
244
                    new FieldError(
4✔
245
                        e.getField(),
1✔
246
                        e.getRejectedValue() == null ? null : e.getRejectedValue().toString(),
9✔
247
                        e.getDefaultMessage()))
2✔
248
            .toList();
2✔
249

250
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
251
    var fieldErrorString =
1✔
252
        fieldErrors.stream()
2✔
253
            .map(f -> "\"" + f.getFieldName() + "\": \"" + f.getValue() + "\"")
8✔
254
            .collect(Collectors.joining(", "));
4✔
255
    var validationException =
8✔
256
        new ValidationException(
257
            "Field validation error on fields: " + fieldErrorString, ex, fieldErrors);
258

259
    logAndCountWarning(validationException, httpStatus);
4✔
260
    var clientResponse = validationException.toClientResponse();
3✔
261
    return handleExceptionInternal(
8✔
262
        validationException, clientResponse, headers, httpStatus, request);
263
  }
264

265
  /**
266
   * 404
267
   *
268
   * <p>When no handler is found for the request.
269
   *
270
   * @param ex The exception
271
   * @param headers The headers
272
   * @param status The status
273
   * @param request The request
274
   */
275
  @Override
276
  protected ResponseEntity<Object> handleNoHandlerFoundException(
277
      @NotNull NoHandlerFoundException ex,
278
      @NotNull HttpHeaders headers,
279
      @NotNull HttpStatusCode status,
280
      @NotNull WebRequest request) {
281
    var httpStatus = HttpStatus.NOT_FOUND;
2✔
282
    var uri =
283
        (request instanceof ServletWebRequest servletWebRequest)
6!
284
            ? servletWebRequest.getRequest().getRequestURI()
4✔
285
            : request.getDescription(false);
1✔
286
    var notFoundException = new NotFoundException("No handler found: " + uri, ex);
7✔
287
    logAndCountWarning(notFoundException, httpStatus);
4✔
288
    var clientResponse = notFoundException.toClientResponse();
3✔
289
    return handleExceptionInternal(notFoundException, clientResponse, headers, httpStatus, request);
8✔
290
  }
291

292
  /**
293
   * Path-variable validation errors. Most likely non-existent IDs.
294
   *
295
   * @param ex The exception
296
   * @param headers The headers
297
   * @param status The status
298
   * @param request The request
299
   */
300
  @Override
301
  protected ResponseEntity<Object> handleHandlerMethodValidationException(
302
      @NotNull HandlerMethodValidationException ex,
303
      @NotNull HttpHeaders headers,
304
      @NotNull HttpStatusCode status,
305
      @NotNull WebRequest request) {
306

307
    var notFound = false;
2✔
308
    for (var error : ex.getAllErrors()) {
11✔
309
      var defaultMessage = error.getDefaultMessage();
3✔
310
      if (defaultMessage != null && defaultMessage.contains("not found")) {
6!
311
        notFound = true;
2✔
312
        break;
1✔
313
      }
314
    }
1✔
315

316
    // 404
317
    if (notFound) {
2✔
318
      var httpStatus = HttpStatus.NOT_FOUND;
2✔
319
      var notFoundException =
4✔
320
          new NotFoundException("Not found: " + request.getDescription(false), ex);
5✔
321
      logAndCountWarning(notFoundException, httpStatus);
4✔
322
      var clientResponse = notFoundException.toClientResponse();
3✔
323
      return handleExceptionInternal(
8✔
324
          notFoundException, clientResponse, headers, httpStatus, request);
325
    }
326

327
    // Bad request
328
    else {
329
      var httpStatus = HttpStatus.BAD_REQUEST;
2✔
330

331
      var errorMessages = buildValidationMessages(ex);
4✔
332
      var userMessage = summarizeValidationMessages(errorMessages);
4✔
333
      var badRequestException = new BadRequestException(userMessage, ex);
6✔
334
      var exceptionName = badRequestException.getClass().getSimpleName();
4✔
335
      log.atWarn()
3✔
336
          .setMessage(badRequestException.getMessage())
4✔
337
          .addKeyValue(RESPONSE_STATUS, String.valueOf(httpStatus))
4✔
338
          .addKeyValue("validationErrors", errorMessages)
4✔
339
          .addKeyValue("request", request.getDescription(false))
2✔
340
          .log();
1✔
341
      meterRegistry
21✔
342
          .counter(
1✔
343
              METRIC_NAME,
344
              METRIC_LEVEL_KEY,
345
              METRIC_LEVEL_WARNING,
346
              METRIC_EXCEPTION_KEY,
347
              exceptionName)
348
          .increment();
1✔
349
      var clientResponse = badRequestException.toClientResponse();
3✔
350
      return handleExceptionInternal(
8✔
351
          badRequestException, clientResponse, headers, httpStatus, request);
352
    }
353
  }
354

355
  /**
356
   * JSON parse errors
357
   *
358
   * @param ex The exception
359
   * @param headers The headers
360
   * @param status The status
361
   * @param request The request
362
   */
363
  @Override
364
  protected ResponseEntity<Object> handleHttpMessageNotReadable(
365
      @NotNull HttpMessageNotReadableException ex,
366
      @NotNull HttpHeaders headers,
367
      @NotNull HttpStatusCode status,
368
      @NotNull WebRequest request) {
369

370
    if (ex.getCause() instanceof JsonParseException jsonParseException
9!
371
        && jsonParseException.getCause() instanceof BadRequestException badRequestException) {
9✔
372
      return handleException(badRequestException);
4✔
373
    }
374

375
    var httpStatus = HttpStatus.BAD_REQUEST;
2✔
376
    var badRequestException = new BadRequestException(ex.getMessage(), ex);
7✔
377
    logAndCountWarning(badRequestException, httpStatus);
4✔
378

379
    // Don't send standard message, HttpMessageNotReadableException may contain valuable info:
380
    var censoredBadRequestException = new BadRequestException("Failed to parse the request.", ex);
6✔
381
    var clientResponse = censoredBadRequestException.toClientResponse();
3✔
382
    return handleExceptionInternal(
8✔
383
        badRequestException, clientResponse, headers, httpStatus, request);
384
  }
385

386
  @Override
387
  protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
388
      HttpRequestMethodNotSupportedException ex,
389
      HttpHeaders headers,
390
      HttpStatusCode status,
391
      WebRequest request) {
392

393
    var supportedMethods = ex.getSupportedHttpMethods();
3✔
394
    var methodNotAllowedException =
3✔
395
        new MethodNotAllowedException(
396
            "Method "
397
                + ex.getMethod()
6✔
398
                + " is not supported for this endpoint. Supported methods are: "
399
                + supportedMethods);
400

401
    // Add Allow header with supported methods
402
    if (supportedMethods != null) {
2!
403
      headers.setAllow(supportedMethods);
3✔
404
    }
405

406
    var errorResponse = methodNotAllowedException.toClientResponse();
3✔
407
    return handleExceptionInternal(
8✔
408
        methodNotAllowedException, errorResponse, headers, status, request);
409
  }
410

411
  private List<String> buildValidationMessages(HandlerMethodValidationException ex) {
412
    return ex.getAllErrors().stream()
6✔
413
        .map(this::resolveValidationMessage)
2✔
414
        .filter(message -> message != null && !message.isBlank())
9!
415
        .toList();
1✔
416
  }
417

418
  private String summarizeValidationMessages(List<String> messages) {
419
    if (messages.isEmpty()) {
3!
420
      return "Validation failed.";
×
421
    }
422
    var limit = 5;
2✔
423
    var summary = messages.stream().limit(limit).collect(Collectors.joining("; "));
10✔
424
    if (messages.size() > limit) {
4✔
425
      summary += "; and " + (messages.size() - limit) + " more.";
7✔
426
    }
427
    return "Validation failed: " + summary;
3✔
428
  }
429

430
  private String resolveValidationMessage(MessageSourceResolvable error) {
431
    var defaultMessage = error.getDefaultMessage();
3✔
432
    if (defaultMessage != null && !defaultMessage.isBlank()) {
5!
433
      return defaultMessage;
2✔
434
    }
435
    var codes = error.getCodes();
3✔
436
    if (codes != null && codes.length > 0) {
5!
437
      return codes[0];
4✔
438
    }
439
    return "Validation error";
×
440
  }
441
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc