MAJ terminée. Nous sommes passés en version 14.6.2 . Pour consulter les "releases notes" associées c'est ici :

https://about.gitlab.com/releases/2022/01/11/security-release-gitlab-14-6-2-released/
https://about.gitlab.com/releases/2022/01/04/gitlab-14-6-1-released/

Commit 44074727 authored by Framboise Orange's avatar Framboise Orange
Browse files

The received json payload is push to the kafka topic.

parent 059f5c11
......@@ -13,7 +13,7 @@
<groupId>fr.gouv.tac</groupId>
<artifactId>analytics-server</artifactId>
<version>1.1.0-SNAPSHOT</version>
<version>1.0.0</version>
<packaging>jar</packaging>
......
......@@ -6,10 +6,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.tac.analytics.server.model.kafka.Analytics;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.common.serialization.StringSerializer;
......@@ -21,16 +19,15 @@ public class KafkaConfiguration {
private String defaultTopic;
@Bean
public KafkaTemplate<String, Analytics> analyticsKafkaTemplate(final ObjectMapper objectMapper, final ProducerFactory defaultSpringProducerFactory) {
public KafkaTemplate<String, String> analyticsKafkaTemplate(final ObjectMapper objectMapper, final ProducerFactory defaultSpringProducerFactory) {
log.debug("conf default producer factory : {}", defaultSpringProducerFactory.getConfigurationProperties().toString());
//a custom producer factory is instantiated in order to take advantage of the spring object mapper on date time management
final ProducerFactory<String, Analytics> producerFactory = new DefaultKafkaProducerFactory<>(defaultSpringProducerFactory.getConfigurationProperties(),
final ProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory<>(defaultSpringProducerFactory.getConfigurationProperties(),
new StringSerializer(),
new JsonSerializer<Analytics>(objectMapper));
new StringSerializer());
final KafkaTemplate<String, Analytics> analyticsKafkaTemplate = new KafkaTemplate<>(producerFactory);
final KafkaTemplate<String, String> analyticsKafkaTemplate = new KafkaTemplate<>(producerFactory);
analyticsKafkaTemplate.setDefaultTopic(defaultTopic);
return analyticsKafkaTemplate;
}
......
package fr.gouv.tac.analytics.server.controller;
import javax.inject.Inject;
import javax.validation.Valid;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.validation.constraints.NotEmpty;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import fr.gouv.tac.analytics.server.controller.mapper.AnalyticsMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.tac.analytics.server.controller.vo.AnalyticsVo;
import fr.gouv.tac.analytics.server.service.AnalyticsService;
import fr.gouv.tac.analytics.server.utils.UriConstants;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping(value = "${analyticsserver.controller.path.prefix}")
@RequiredArgsConstructor(onConstructor = @__(@Inject))
@RequestMapping(value = "${analyticsserver.controller.path.prefix}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public class AnalyticsController {
private final AnalyticsService analyticsService;
private final AnalyticsMapper analyticsMapper;
private final ObjectMapper objectMapper;
private final Validator validator;
public AnalyticsController(AnalyticsService analyticsService, ObjectMapper objectMapper, Validator validator) {
this.analyticsService = analyticsService;
this.objectMapper = objectMapper;
this.validator = validator;
}
@PostMapping(path = UriConstants.API_V1 + UriConstants.ANALYTICS)
public ResponseEntity<Void> addAnalytics(@Valid @RequestBody final AnalyticsVo analyticsVo) {
analyticsMapper.map(analyticsVo).ifPresent(analyticsService::createAnalytics);
public ResponseEntity<Void> addAnalytics(@NotEmpty @RequestBody String analyticsAsJson) throws JsonProcessingException {
AnalyticsVo analyticsVo = objectMapper.readValue(analyticsAsJson, AnalyticsVo.class);
Set<ConstraintViolation<AnalyticsVo>> constraintViolationSet = validator.validate(analyticsVo);
if (! constraintViolationSet.isEmpty()) {
throw new ConstraintViolationException(constraintViolationSet);
}
analyticsService.createAnalytics(analyticsAsJson);
return ResponseEntity.ok().build();
}
......
......@@ -2,13 +2,15 @@ package fr.gouv.tac.analytics.server.controller;
import java.time.ZonedDateTime;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.fasterxml.jackson.core.JsonProcessingException;
import fr.gouv.tac.analytics.server.controller.vo.ErrorVo;
import lombok.extern.slf4j.Slf4j;
......@@ -23,8 +25,8 @@ public class CustomExceptionHandler {
return errorVoBuilder(e, HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<ErrorVo> exception(final MethodArgumentNotValidException e) {
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity<ErrorVo> exception(final ConstraintViolationException e) {
if (e.getMessage().contains(PAYLOAD_TOO_LARGE)) {
// log dedicated to raised an alarm from supervision
......@@ -35,6 +37,11 @@ public class CustomExceptionHandler {
}
}
@ExceptionHandler(value = JsonProcessingException.class)
public ResponseEntity<ErrorVo> exception(final JsonProcessingException e) {
return errorVoBuilder(e, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<ErrorVo> exception(final Exception e) {
log.warn("Unexpected error :", e);
......
package fr.gouv.tac.analytics.server.controller.mapper;
import fr.gouv.tac.analytics.server.controller.vo.AnalyticsVo;
import fr.gouv.tac.analytics.server.model.kafka.Analytics;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.util.Optional;
@Mapper(componentModel = "spring", uses = TimestampedEventMapper.class)
public interface AnalyticsMapper {
default Optional<Analytics> map(final AnalyticsVo analyticsVo) {
return Optional.ofNullable(this.mapInternal(analyticsVo));
}
@Mapping(target = "creationDate", expression = "java( java.time.ZonedDateTime.now() )")
Analytics mapInternal(final AnalyticsVo analyticsVo);
}
package fr.gouv.tac.analytics.server.controller.mapper;
import fr.gouv.tac.analytics.server.model.kafka.TimestampedEvent;
import fr.gouv.tac.analytics.server.controller.vo.TimestampedEventVo;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface TimestampedEventMapper {
TimestampedEvent map(TimestampedEventVo timestampedEventVo);
}
package fr.gouv.tac.analytics.server.model.kafka;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Analytics {
private String installationUuid;
private Map<String, String> infos;
private List<TimestampedEvent> events;
private List<TimestampedEvent> errors;
private ZonedDateTime creationDate;
}
package fr.gouv.tac.analytics.server.model.kafka;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class TimestampedEvent {
private String name;
private ZonedDateTime timestamp;
private String desc;
}
package fr.gouv.tac.analytics.server.service;
import fr.gouv.tac.analytics.server.model.kafka.Analytics;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.inject.Inject;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFutureCallback;
import javax.inject.Inject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Inject))
public class AnalyticsService {
private final KafkaTemplate<String, Analytics> kafkaTemplate;
private final KafkaTemplate<String, String> kafkaTemplate;
public void createAnalytics(final Analytics analytics) {
kafkaTemplate.sendDefault(analytics).addCallback(new ListenableFutureCallback<>() {
public void createAnalytics(final String analyticsAsJson) {
kafkaTemplate.sendDefault(analyticsAsJson).addCallback(new ListenableFutureCallback<>() {
@Override
public void onFailure(final Throwable throwable) {
log.warn("Error sending message to kafka", throwable);
}
@Override
public void onSuccess(final SendResult<String, Analytics> sendResult) {
public void onSuccess(final SendResult<String, String> sendResult) {
log.debug("Message successfully sent {}", sendResult);
}
});
......
package fr.gouv.tac.analytics.server.controller;
import fr.gouv.tac.analytics.server.controller.mapper.AnalyticsMapper;
import javax.validation.Validator;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.gouv.tac.analytics.server.service.AnalyticsService;
import fr.gouv.tac.analytics.server.controller.vo.AnalyticsVo;
import fr.gouv.tac.analytics.server.model.kafka.Analytics;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Optional;
@ExtendWith(SpringExtension.class)
public class AnalyticsControllerTest {
......@@ -23,24 +23,28 @@ public class AnalyticsControllerTest {
private AnalyticsService analyticsService;
@Mock
private AnalyticsMapper analyticsMapper;
private ObjectMapper objectMapper;
@Mock
private Validator validator;
@InjectMocks
private AnalyticsController analyticsController;
@Test
public void shouldCreateAnalytics() {
public void shouldCreateAnalytics() throws JsonProcessingException {
final AnalyticsVo analyticsVo = new AnalyticsVo();
final Optional<Analytics> analytics = Optional.of(new Analytics());
final String analyticsAsString = "";
Mockito.when(analyticsMapper.map(analyticsVo)).thenReturn(analytics);
final ResponseEntity<Void> result = analyticsController.addAnalytics(analyticsVo);
final ResponseEntity<Void> result = analyticsController.addAnalytics(analyticsAsString);
Assertions.assertThat(result).isNotNull();
Assertions.assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
Mockito.verify(analyticsService).createAnalytics(analytics.get());
Mockito.verify(analyticsService).createAnalytics(analyticsAsString);
}
//TODO ==> ADD TEST IN CASE VALIDATION ERROR ==> RAISING EXCEPTION
}
\ No newline at end of file
......@@ -4,13 +4,15 @@ import static fr.gouv.tac.analytics.server.controller.CustomExceptionHandler.PAY
import java.time.ZonedDateTime;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.bind.MethodArgumentNotValidException;
import com.fasterxml.jackson.core.JsonProcessingException;
import fr.gouv.tac.analytics.server.controller.vo.ErrorVo;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
......@@ -23,7 +25,10 @@ import org.mockito.Mockito;
public class CustomExceptionHandlerTest {
@Mock
private MethodArgumentNotValidException methodArgumentNotValidException;
private ConstraintViolationException constraintViolationException;
@Mock
private JsonProcessingException jsonProcessingException;
@InjectMocks
private CustomExceptionHandler customExceptionHandler;
......@@ -40,24 +45,36 @@ public class CustomExceptionHandlerTest {
}
@Test
public void shouldManageMethodArgumentNotValidException() {
public void shouldManageConstraintViolationException() {
final String message = "error message";
Mockito.when(constraintViolationException.getMessage()).thenReturn(message);
final ResponseEntity<ErrorVo> result = customExceptionHandler.exception(constraintViolationException);
Assertions.assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
Assertions.assertThat(result.getBody().getMessage()).isEqualTo(message);
Assertions.assertThat(result.getBody().getTimestamp()).isEqualToIgnoringSeconds(ZonedDateTime.now());
}
@Test
public void shouldManageJsonProcessingException() {
final String message = "error message";
Mockito.when(methodArgumentNotValidException.getMessage()).thenReturn(message);
Mockito.when(jsonProcessingException.getMessage()).thenReturn(message);
final ResponseEntity<ErrorVo> result = customExceptionHandler.exception(methodArgumentNotValidException);
final ResponseEntity<ErrorVo> result = customExceptionHandler.exception(jsonProcessingException);
Assertions.assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
Assertions.assertThat(result.getBody().getMessage()).isEqualTo(message);
Assertions.assertThat(result.getBody().getTimestamp()).isEqualToIgnoringSeconds(ZonedDateTime.now());
}
@Test
public void shouldManageMethodArgumentNotValidExceptionInCaseOfTooLargePayload() {
public void shouldManageConstraintViolationExceptionInCaseOfTooLargePayload() {
final String message = PAYLOAD_TOO_LARGE + " - error message";
Mockito.when(methodArgumentNotValidException.getMessage()).thenReturn(message);
Mockito.when(constraintViolationException.getMessage()).thenReturn(message);
final ResponseEntity<ErrorVo> result = customExceptionHandler.exception(methodArgumentNotValidException);
final ResponseEntity<ErrorVo> result = customExceptionHandler.exception(constraintViolationException);
Assertions.assertThat(result.getStatusCode()).isEqualTo(HttpStatus.PAYLOAD_TOO_LARGE);
Assertions.assertThat(result.getBody().getMessage()).isEqualTo(message);
Assertions.assertThat(result.getBody().getTimestamp()).isEqualToIgnoringSeconds(ZonedDateTime.now());
......
package fr.gouv.tac.analytics.server.controller.mapper;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.util.ReflectionTestUtils;
import fr.gouv.tac.analytics.server.controller.vo.AnalyticsVo;
import fr.gouv.tac.analytics.server.controller.vo.TimestampedEventVo;
import fr.gouv.tac.analytics.server.model.kafka.Analytics;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(SpringExtension.class)
public class AnalyticsMapperTest {
private static final AnalyticsMapper mapper = new AnalyticsMapperImpl();
@BeforeAll
public static void setUp() {
final TimestampedEventMapper timestampedEventMapper = new TimestampedEventMapperImpl();
ReflectionTestUtils.setField(mapper, "timestampedEventMapper", timestampedEventMapper);
}
@Test
public void shouldFailWhenAnalyticsIsNull() {
// Given
final AnalyticsVo analyticsVo = null;
// When
final Optional<Analytics> result = mapper.map(analyticsVo);
// Then
Assertions.assertThat(result).isEmpty();
}
@Test
public void shouldSucceedWhenEventsIsNull() {
// Given
final AnalyticsVo analyticsVo = getAnalytics();
analyticsVo.setEvents(null);
// When
final Optional<Analytics> result = mapper.map(analyticsVo);
// Then
Assertions.assertThat(result).isPresent();
Assertions.assertThat(result.get().getCreationDate()).isEqualToIgnoringSeconds(ZonedDateTime.now());
Assertions.assertThat(result.get().getInstallationUuid()).isEqualTo(analyticsVo.getInstallationUuid());
Assertions.assertThat(result.get().getInfos()).containsExactlyInAnyOrderEntriesOf(analyticsVo.getInfos());
Assertions.assertThat(result.get().getEvents()).isNull();
Assertions.assertThat(result.get().getErrors()).hasSameSizeAs(analyticsVo.getErrors());
}
@Test
public void shouldSucceedWhenErrorsIsNull() {
// Given
final AnalyticsVo analyticsVo = getAnalytics();
analyticsVo.setErrors(null);
// When
final Optional<Analytics> result = mapper.map(analyticsVo);
// Then
Assertions.assertThat(result).isPresent();
Assertions.assertThat(result.get().getCreationDate()).isEqualToIgnoringSeconds(ZonedDateTime.now());
Assertions.assertThat(result.get().getInstallationUuid()).isEqualTo(analyticsVo.getInstallationUuid());
Assertions.assertThat(result.get().getInfos()).containsExactlyInAnyOrderEntriesOf(analyticsVo.getInfos());
Assertions.assertThat(result.get().getEvents()).hasSameSizeAs(analyticsVo.getEvents());
Assertions.assertThat(result.get().getErrors()).isNull();
}
@Test
public void shouldSucceedWhenAnalyticsIsValid() {
// Given
final AnalyticsVo analyticsVo = getAnalytics();
// When
final Optional<Analytics> result = mapper.map(analyticsVo);
// Then
Assertions.assertThat(result).isPresent();
Assertions.assertThat(result.get().getCreationDate()).isEqualToIgnoringSeconds(ZonedDateTime.now());
Assertions.assertThat(result.get().getInstallationUuid()).isEqualTo(analyticsVo.getInstallationUuid());
Assertions.assertThat(result.get().getInfos()).containsExactlyInAnyOrderEntriesOf(analyticsVo.getInfos());
Assertions.assertThat(result.get().getEvents()).hasSameSizeAs(analyticsVo.getEvents());
Assertions.assertThat(result.get().getErrors()).hasSameSizeAs(analyticsVo.getErrors());
}
public AnalyticsVo getAnalytics() {
final Map<String, String> infos = Map.of("info1", "info1Value", "info2", "info2value");
final List<TimestampedEventVo> analyticsEvents = new ArrayList<>();
final List<TimestampedEventVo> analyticsErrors = new ArrayList<>();
final TimestampedEventVo event = TimestampedEventVo.builder().name("userAcceptedNotificationsInOnboarding").timestamp(ZonedDateTime.now()).desc("some description").build();
final TimestampedEventVo error = TimestampedEventVo.builder().name("ERR432").timestamp(ZonedDateTime.now()).build();
analyticsEvents.add(event);
analyticsErrors.add(error);
return AnalyticsVo.builder()
.infos(infos)
.events(analyticsEvents)
.errors(analyticsErrors)
.build();
}
}
package fr.gouv.tac.analytics.server.controller.mapper;
import java.time.ZonedDateTime;
import fr.gouv.tac.analytics.server.controller.vo.TimestampedEventVo;
import fr.gouv.tac.analytics.server.model.kafka.TimestampedEvent;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class TimestampedEventMapperTest {
private final TimestampedEventMapper timestampedEventMapper = new TimestampedEventMapperImpl();
@Test
public void shouldMapWithDescription() {
final TimestampedEventVo timestampedEventVo = TimestampedEventVo.builder()
.name("some fancy name")
.timestamp(ZonedDateTime.now())
.desc("some description")
.build();
final TimestampedEvent result = timestampedEventMapper.map(timestampedEventVo);
Assertions.assertThat(result.getName()).isEqualTo(timestampedEventVo.getName());
Assertions.assertThat(result.getTimestamp()).isEqualTo(timestampedEventVo.getTimestamp());
Assertions.assertThat(result.getDesc()).isEqualTo(timestampedEventVo.getDesc());
}