Используемые технологии

Технологический стек:

  • Java 21;

  • Spring boot;

  • Spring webFlux

  • Maven;

  • WebClient;

Какую пользу принесет эта статья?

Эта статья предлагает вариант реализации клиента к  Graph Ql api. В предлагаемом решении:

  • Разложим создаваемый клиент по слоям;

  • Реализуем тесты для созданного кода;

  • Имплементируем клиент в бизнес сервис;

Если решение заинтересовало, движемся дальше.

Немного контекста

Команда занимается развитием сервисов и микросервисов, которые собирают данные для расчета предложения по страховым продуктам.  Подробный рассказ в этой статье. Интеграции с внутренними и внешними источниками постоянно пополняются. С Graph Ql столкнулись впервые. Долго времени на осознание сути взаимодействия не понадобилось. Graph Ql - это язык запросов и протокол для API, созданный командой Facebook в 2012 году. Он предоставляет разработчикам гибкий способ взаимодействия. Graph Ql решает следующие задачи:

  • Позволяет получать именно те данные, которые нужны;

  • Объединяет множество ресурсов в единый запрос;

  • Предоставляет понятную и строгую систему типов;

  • Минимизирует количество запросов к серверу;

Мне требовалось использовать конкретный endpoint для загрузки данных по участию в аукционах вторичных транспортных средств. Если ваша задача шире, рекомендую глубже вникнуть в систему типов и схем Graph Ql. Это потребуется на стадии загрузки ��азных типов данных. Ссылки на материал оставил выше.

Из чего выбирали?

Моя команда предпочитает использовать feign в качестве клиента для синхронного и реактивного взаимодействия. О деталях использования и сравнении реактивной и синхронной библиотеки рассказывал ранее. Профильную библиотеку Graph Ql, для feign я нашел только эту. О ней мало информации, последние обновления около 6 лет назад, есть открытые задачи. Использовать ее не стал. Если знаете надежное решение - помогите, оставьте комментарий. Обратимся к документации. Spring boot рекомендует использовать следующие клиенты:

Особняком стоит космолет Netflix DGS, который намеренно обойдем вниманием. Это проект из недр одноименной компании. Он содержит много функциональности для высоконагруженных систем, базирующихся на spring компонентах. Все пакеты и части Netflix решений, с которыми я сталкивался заслуживают особого внимания. Они готовы для использования под высокими нагрузками, но освоение и конфигурирование этих решений требуют отдельного внимания. Здесь говорить о них не будем. Если потребуется глубоко и основательно разбираться с серверными механизмами Graph Ql, то сделаем по ним отдельную статью. Сейчас наша цель - сделать надежное решение без освоения деталей конкретной прикладной технологии. Давайте проведем небольшое исследование и сравним между собой (таб.1) рекомендованные spring, Graph Ql библиотеки.

Таб.1 - Сравнение Graph Ql клиентов

Клиент

Преимущества

Недостатки

Вывод

HttpSyncGraphQlClient

Простое конфигурирование

Однозначное поведение при обработке ошибок

Только синхронный режим

Оптимально, если нет требований к возрастающей нагрузке и сложному конфигурированию поведения

HttpGraphQlClient

Поддержка синхронности, реактивности

Поддержка WebFlux (Интегрирован в Spring Boot)

Широкие возможности конфигурирования

Сложность при освоении

Сложность при отладке

Создает дополнительный архитектурный слой

Для использования в рамках средних по сложности проектов в экосистеме spring boot, с возможной поддержкой реактивности и асинхронности

WebSocketGraphQlClient

Заточен под real-time (webSocket)

Работа по шаблону "Подписчик-Издатель"

Сложности в поддержке: сеть, firewall, прокси

Проблемы с маштабируемостью и отказоустойчивостью

Для real-time обмен данными

RSocketGraphQlClient

Поддержка многих способ интеграции (streaming, запрос-ответ)

Встроенные механизмы балансировки и нагрузки (backpressure)

Сложность настройки, поддержки

Ограниченная распространенность в сравнении с конкурентами

Специализированное сетевое окружение

Для сложных систем с требованием высокой производительности и гибкости обмена сообщениями

Для наших задач оптимальным выглядит использование HttpGraphQlClient:

  • Мы используем традиционный синхронный spring boot стек с рефакторингом на реактивность и асинхронность микросервисов и сервисов там, где это обосновано по профилю нагрузки:

    • Если что-то простое - делаем просто, синхронно, с использованием map-struct, validator, spring-data, spring-security, postgre, redis, kafka и прочих элементов экосистемы spring. Масштабируемся с помощью k8s;

    • Если элемент по профилю становится похож на высоконагруженную часть системы - используем реактивное программирование, параллельную обработку, асинхронность;

  • Разработка, внедрение, поддержка, администрирование, развитие сервисов и инфраструктурных компонентов лежит в сфере ответственности команды:

    • Сами делаем, сами разворачиваем, поддерживаем и развиваем. Для сложных вопросов есть DevOps. Команд много, devOps не очень;

  • Система, состоит из backend компонентов, назначение которых сбор и представление данных:

    • Преимущественно команда не занимается развитием UI систем и компонентов. Есть исключения, которые только подчеркивают общий тренд;

Наша задача - подключение нового источника с возможным рефакторингом на реактивный режим работы. В этом случае оптимальный выбор -HttpGraphQlClient.

Задача

Полная Формулировка задачи будет такой:

  • Необходимо скачать данные из системы A и передать их в систему B;

  • Данные с бизнес признаком isCompleted == true необходимо кешировать на месяц, в противном случае, если ответ получен, данные кешируются на 5 минут;

  • Все загруженные из внешнего источника данные необходимо записывать в топик для проведения сверок;

  • Система A предполагает аутентификацию по OAuth 2.0;

  • Разработанная системе/сервис должна поддерживать аутентификацию с помощью keyclock или другой IAM системы; 

Целевой процесс (Рис.1).

Рис.1
Рис.1

Дальше мы сконцентрируемся на первой части - получить данные сервера Graph Ql. В приложенном проекте будет функциональность всей задачи, но подробно мы будем говорить только про первую часть.

Рабочий сервис

Конфигурация клиента

GraphQL — протокол уровня прикладного взаимодействия с данными. Это альтернативный способ взаимодействия с API по сравнению с REST. Он не определяет способ транспортировки и, в теории, может существовать даже поверх рукописных писем или поверх любого сетевого протокола. Основная цель - сделать запросы к данным более гибкими и удобными. В REST обычно закреплены фиксированные маршруты (endpoints) для каждого ресурса и возвращается набор данных. В Graph Ql запрос формируется с указанием, какие именно данные нужны. Сервер возвращает только их. Graph Ql дополняет и улучшает некоторые моменты работы с данными по сравнению с REST. В контексте модели OSI — это уровень прикладного протокола, который базируется на сетевом протоколе. В нашем случае это будет HttpClient. Мы возьмем тот, который рекомендуется spring boot, то есть reactor.netty, который будет поддерживать синхронный и реактивный способ взаимодействия. Для HttpClient установим следующие параметры:

  • Тайм-аут на установку TCP соединения с сервером - connectTimeout:

    • Можно установить еще кучу параметров (30) из профильного класса. Мы ограничимся только этим;

  • Тайм-аут на время ответа от сервера - responseTimeout;

  • Флаг перенаправления запроса - followRedirect (реакция на HttpStatus 3XX) ;

  • Подробное логирование сетевого траффика - wiretap;

  • Дополнительные обработчики на установленное соединение - doOnConnected;

Параметров у нас много. Вынесем их в application.yml и будем использовать там, где требуется.

Слой HttpClient

Сконфигурированный клиент.

@Configuration
@RequiredArgsConstructor
public class HttpClientConfig {

    private final ClientCustomProperties clientCustomProperties;

    @Bean
    public HttpClient getHttpClient() {

        final Settings settings = clientCustomProperties.settings();

        return HttpClient.create()
                .option(
                    ChannelOption.CONNECT_TIMEOUT_MILLIS, 
                    settings.connectCreateTimeoutMillis()
                )
                .responseTimeout(Duration.ofMillis(settings.responseGetTimeMillis()))
                .followRedirect(settings.followRedirect())
                .wiretap(settings.wiretap())
                .doOnConnected(connection ->
                        connection
                                .addHandlerLast(
                                        new ReadTimeoutHandler(
                                                settings.readTimeMillis(),
                                                TimeUnit.MILLISECONDS)
                                )
                                .addHandlerLast(
                                        new WriteTimeoutHandler(
                                                settings.writeTimeMillis(),
                                                TimeUnit.MILLISECONDS)
                                )
                );
    }
}

По ходу объяснений на каждый компонент будем предлагать адекватный тестовый класс. Параметры подключения растекаются по иерархии классов и интерфейсов, реализующих соединение. Часть из них у нас не будет возможности проверить в тестовом классе без применения рефлексивной магии. Оставим ее пока. Тем более, что само подключение мы проверим позже, в интеграционном тесте. Сейчас проверим все, что лежит на поверхности.

@ExtendWith(MockitoExtension.class)
class HttpClientConfigTest {

    private HttpClientConfig httpClientConfig;

    @Mock
    private ClientCustomProperties clientCustomProperties;

    @BeforeEach
    void setUp() {
        httpClientConfig = new HttpClientConfig(clientCustomProperties);
    }

    @Test
    void checkHttpClientConfig() {
        assertThat(httpClientConfig)
                .hasFieldOrPropertyWithValue(
                  "clientCustomProperties", 
                  clientCustomProperties);
    }

    @Test
    void getHttpClient_shouldReturnClient() {
        final Settings settings = TestData.getTestSettings();

        when(clientCustomProperties.settings())
                .thenReturn(settings);

        assertThat(httpClientConfig.getHttpClient())
                .isInstanceOfSatisfying(HttpClient.class, httpClient -> {

                    assertThat(httpClient.configuration().options())
                            .containsKey(ChannelOption.CONNECT_TIMEOUT_MILLIS);

                    assertThat(httpClient.configuration().responseTimeout())
                          .isEqualTo(
                              Duration.ofMillis(settings.responseGetTimeMillis())
                          );
                });

        verify(clientCustomProperties).settings();
    }

}

C HttpClient мы закончили. Этой конфигурации для решения нашей задачи будет достаточно.

Слой WebClient

Теперь на ее основе используем WebClient, поверх которого мы построим HttpGraphQlClient соединение. Для WebClient потребуется обработчик ошибок подключения, который будет основан на логике анализа статусов сообщений, для которого нам понадобится создать профильные исключения. Функциональность обработки ошибочных статусов будет однозначной. Сами исключения будут содержать только описание ошибки. Покажу логику обработки статусов, но не буду ей уделять много внимания. WebClient планирую сделать на основе Mono. Говорил о том, что не буду переусложнять компонент. Алгоритм обработки предполагает что будем получать данные от сервера и потом отправлять их в Redis и Kafka. Если будем использовать реактивные типы данных, то можно реализовать параллельность обработки, после этого блокировать рабочий поток и выдавать результат в классическом стиле. Подытожу - сервис будет обрабатывать сообщения на tomcat, а внутри обрабатывать сообщения с помощью Mono(netty).

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(value = ClientCustomProperties.class)
public class WebClientConfig {

    private final GraphQlClientMessageHandler graphQlClientMessageHandler;
    private final ClientCustomProperties clientCustomProperties;
    private final HttpClientConfig httpClientConfig;


    @Bean
    public WebClient createConnection() {

        return WebClient.builder()
                .baseUrl(clientCustomProperties.service().url())
                .clientConnector(
                      new ReactorClientHttpConnector(httpClientConfig.getHttpClient()))
                .defaultHeader(
                      HttpHeaders.CONTENT_TYPE, 
                      MediaType.APPLICATION_JSON_VALUE)
                .filter(graphQlClientMessageHandler.errorHandler())
                .build();
    }

}

Мы используем данные для установки соединения и компоненты для обработки ошибочных запросов. В тесте используем немного магии ReflectionTestUtils и проверим, что установленные нами параметры соединения были переданы в используемые нами библиотеки http.

@ExtendWith(MockitoExtension.class)
class WebClientConfigTest {

    private WebClientConfig webClientConfig;

    @Mock
    private GraphQlClientMessageHandler graphQlClientMessageHandler;

    @Mock
    private ClientCustomProperties clientCustomProperties;

    @Mock
    private HttpClientConfig httpClientConfig;

    @BeforeEach
    void setUp() {

        webClientConfig = new WebClientConfig(
          graphQlClientMessageHandler, 
          clientCustomProperties, 
          httpClientConfig);

    }

    @Test
    void checkWebClientConfig() {
        assertThat(webClientConfig)
                .hasFieldOrPropertyWithValue(
                    "graphQlClientMessageHandler", 
                    graphQlClientMessageHandler)
                .hasFieldOrPropertyWithValue(
                    "clientCustomProperties", 
                    clientCustomProperties)
                .hasFieldOrPropertyWithValue(
                    "httpClientConfig", 
                    httpClientConfig)
        ;
    }

    @Test
    void create_shouldReturnWebClient() {
        final String url = TEST_STRING;
        final ExchangeFilterFunction exchangeFilterFunction = 
          mock(ExchangeFilterFunction.class);
        final HttpClient httpClient = mock(HttpClient.class);

        when(httpClientConfig.getHttpClient())
                .thenReturn(httpClient);

        when(graphQlClientMessageHandler.errorHandler())
                .thenReturn(exchangeFilterFunction);

        when(clientCustomProperties.service())
                .thenReturn(Service.builder().url(url).build());

        assertThat(webClientConfig.createConnection())
                .isInstanceOfSatisfying(WebClient.class, webClient -> {

                    assertThat(
                            ReflectionTestUtils.getField(
                                    ReflectionTestUtils.getField(webClient, "builder"),
                                    "baseUrl")
                    )
                            .isEqualTo(url);

                    assertThat(
                            ReflectionTestUtils.getField(
                                    ReflectionTestUtils.getField(webClient, "builder"),
                                    "connector")
                    )
                            .isInstanceOf(ReactorClientHttpConnector.class);

                    assertThat((HttpClient)
                            ReflectionTestUtils.getField(
                                    ReflectionTestUtils.getField(
                                            ReflectionTestUtils.getField(
                                              webClient, "builder"),
                                            "connector"),
                                    "httpClient")
                    )
                            .isInstanceOfSatisfying(HttpClient.class, 
                                                      httpClientData ->

                                    assertThat(httpClientData)
                                            .usingRecursiveComparison()
                                            .isEqualTo(httpClient)

                            );

                });

        verify(httpClientConfig).getHttpClient();
        verify(graphQlClientMessageHandler).errorHandler();
        verify(clientCustomProperties).service();

    }

}

Обработку ошибочных статусов от сервера мы построили на основе ExchangeFilterFunction. Это функциональный интерфейс в Spring WebFlux, который является ключевым компонентом для настройки WebClient. Его основная задача — перехватывать и изменять клиентские HTTP-запросы и их ответы в неблокирующем, реактивном стиле. Обработка построена на основе фильтров, с помощью которых можно добавлять аутентификацию и авторизацию, логирование, заголовки, метрики, трассировку, кэширование и т.д. Реализация будет следующей.

@Component
@RequiredArgsConstructor
public class GraphQlClientMessageHandler {

    public final ErrorStatusProcessing errorStatusProcessing;

    public ExchangeFilterFunction errorHandler() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
                    if (clientResponse.statusCode().is4xxClientError()) {
                        return errorStatusProcessing
									.handleErrorResponse(
										clientResponse, 
										GRAPH_QL_CLIENT_CLIENT_MESSAGE
						);
                    }
                    if (clientResponse.statusCode().is5xxServerError()) {
                        return errorStatusProcessing
									.handleErrorResponse(
										clientResponse, 
										GRAPH_QL_CLIENT_SERVER_MESSAGE
						);
                    } else {
                        return Mono.just(clientResponse);
                    }
                }

        );
    }

}

Этот класс, получился лаконичным и однозначным. Берем сообщение, проверяем его заголовок. Если по логике статус или другая информация в заголовке нам не подходит - делаем над ним действия. ErrorStatusProcessing на вход получают само сообщение и тип ошибки, возвращают преобразованное сообщение, обернутое в Mono. Реализация содержит реактивную обработку данных. Ее приводить не буду, если потребуется - в конце будут ссылки на рабочий проект. Тестовый класс на GraphQlClientMessageHandler много места не займет. Проверим логику обработки параметризованным тестом.

Слой Graph Ql 

Основные слои нашего подключения определены HttpClient → WebClient → и переходим к Graph Ql компонентам. Мы будем использовать HttpGraphQlClient. Для нашей задачи - оптимальный выбор, который имеет необходимую гибкость и запас по производительности. В него нам надо будет передать подготовленный WebClient и данные для аутентификации. Для его использования требуется добавить spring-boot-starter-graphql в pom. О компоненте аутентификации раньше вышла отдельная статья. Аутентификация развивалась, была переведена на реактивный стек, дополнилась логикой, с помощью которой возможно использование во многих подключениях внутри одного сервиса. Graph Ql будет таким.

@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(value = ClientCustomProperties.class)
public class GraphQlConfig {

    private final WebClientConfig webClientConfig;

    private final AuthService authService;

    private final ClientCustomProperties clientCustomProperties;

    @Bean
    public HttpGraphQlClient graphQlClientConnection() {
        return HttpGraphQlClient
                .builder(webClientConfig.createConnection())
                .header(
                        HttpHeaders.AUTHORIZATION,
                         authService.getBearerToken(
                         clientCustomProperties.settings().systemName()
                       )
                )
                .build()
                ;
    }

}

Тест будет проверять логику и использование атрибутов.

@ExtendWith(MockitoExtension.class)
class GraphQlConfigTest {

    private GraphQlConfig graphQlConfig;

    @Mock
    private WebClientConfig webClientConfig;

    @Mock
    private AuthService authService;

    @Mock
    private ClientCustomProperties clientCustomProperties;

    @BeforeEach
    void setUp() {
        graphQlConfig =
                new GraphQlConfig(
                    webClientConfig, 
                    authService, 
                    clientCustomProperties
        );
    }

    @Test
    void checkGraphQlConfig() {
        assertThat(graphQlConfig)
                .hasFieldOrPropertyWithValue(
                    "webClientConfig", webClientConfig
                )
                .hasFieldOrPropertyWithValue(
                    "authService", authService
                )
                .hasFieldOrPropertyWithValue(
                    "clientCustomProperties", clientCustomProperties
                )
        ;
    }

    @Test
    void graphQlClientConnection_shouldReturnConfig() {
        final String url = TEST_STRING;
        final String token = TEST_STRING;
        final WebClient webClient = WebClient.builder().build();
        final Settings settings = TestData.getTestSettings();

        when(webClientConfig.createConnection())
                .thenReturn(webClient);

        when(clientCustomProperties.settings())
                .thenReturn(settings);

        when(authService.getBearerToken(settings.systemName()))
                .thenReturn(token);

        assertThat(graphQlConfig.graphQlClientConnection())
                .isInstanceOf(HttpGraphQlClient.class);

        verify(webClientConfig).createConnection();
        verify(clientCustomProperties).settings();
        verify(authService).getBearerToken(settings.systemName());
    }

}

У нас полная конфигурация подключения, которую мы можем использовать в клиенте, назначение которого получить от сервера ответ. Немного деталей о Graph Ql:

  • Graph Ql сервер, традиционно использует один endpoint для всех запросов, в отличие от REST, где обычно разные ресурсы имеют свои адреса;

  • Запросы описываются с помощью специального языка запросов. Это позволяет получать сразу все необходимые данные;

  • REST использует стандартные HTTP методы (GET, POST, PUT, DELETE). Graph Ql обычно использует POST с телом запроса;

В случае, когда требуется получить данные нам надо передать запрос, в котором представлены параметры и обработать ответ. Язык Graph Ql воспринимается мной как Json с элементами sql - строишь запрос, в котором указываешь, какие переменные будешь использовать и что хочешь получить в ответе. Ответ будет настолько содержательным, как определишь. Сейчас нет необходимости динамически изменять запрос и под него формировать переменные. Требуется пара переменных - одна для идентификации моих возможностей по тарифу, вторая для запроса бизнес данных по тс. Это будет вин. Для обработки ответа нам требуется указать уровень ответа, который буде�� преобразован в объект. На этом этапе не происходит блокировка данных, продолжаем работать в реактивный поток.

@Service
@RequiredArgsConstructor
@Slf4j
@EnableConfigurationProperties(value = ClientCustomProperties.class)
public class GraphQlClientService {

    private final GraphQlConfig graphQlConfig;
    private final ClientCustomProperties clientCustomProperties;
    private final ErrorMessageProcessing errorMessageProcessing;

    public Mono<DataPackage> dataCarHistoryByVin(String vin) {

        final Map<String, Object> variables = Map.of(
                ACCESS_ID, clientCustomProperties.connectionData().accessId(),
                VIN, vin
        );

        return graphQlConfig.graphQlClientConnection()
                .document(GraphQlClientStructure.DATA.getDocumentWithVariables())
                .variables(variables)
                .retrieve(GraphQlClientStructure.DATA.getPath())
                .toEntity(DataPackage.class)
                .doOnError(errorMessageProcessing::handleMessageError);
    }

}

Подготовили переменные, отправили запрос в виде строки, дополнив его переменными, указали, что именно ждем в ответ, обработали его в объект, при необходимости разобрались с ошибками. Запрос и ответ находятся в внутреннем справочнике enum GraphQlClientStructure. Этой гибкости будет достаточно для новых уровней ответа или изменения содержимого запроса. Если появится логика, мы готовы ее гибко масштабировать. Обработка ошибок обернута в отдельный класс с уже знакомым нам типом исключения. Он помещается в небольшом классе на 3 строчки. Тест на весь сервис интеграционный, содержательный с необходимостью подготовки многих параметров.

@SpringBootTest
@TestPropertySource(properties = {
        "clients.data.service.url=http://localhost:${wiremock.server.port}",
        "clients.data.connectionData.accessId=accessId",
        "clients.data.settings.connectCreateTimeoutMillis=1000",
        "clients.data.settings.responseGetTimeMillis=1000",
        "clients.data.settings.readTimeMillis=1000",
        "clients.data.settings.writeTimeMillis=1000",
        "clients.data.settings.followRedirect=true",
        "clients.data.settings.systemName=string",
        "clients.data.settings.wiretap=true",
        "partner-attributes.cacheTokenTimeToLiveSeconds=1",
        "partner-attributes.systemDataList[0].string=string",
})
@AutoConfigureWireMock
class GraphQlClientServiceTest {

    @Autowired
    private GraphQlClientService graphQlClientService;

    @MockitoBean
    private ErrorMessageProcessing errorMessageProcessing;

    @MockitoBean
    private AuthService authService;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private KafkaProducerConfig kafkaProducerConfig;

    @MockitoBean
    private KafkaTemplate<String, Object> kafkaTemplate;

    @MockitoBean
    private AuthFeignClient authFeignClient;

    @MockitoBean
    private JwtDecoder jwtDecoder;

    @Test
    void dataCarHistoryByVin_shouldReturnResult() throws JsonProcessingException {

        final String vin = TEST_STRING;

        String prettyGraphQlQuery =
                GraphQlClientStructure.DATA.getDocumentWithVariables()
                        .replace("\"", "\\\"")
                        .replace("\n", "\\n");

        String requestBody = String.format(
                "{\"query\":\"%s\",\"variables\":{\"accessId\":\"accessId\",\"vin\":\"%s\"}}",
                prettyGraphQlQuery,
                vin
        );

        Data data = TestData.getTestCarHistoryData();

        Map<String, Data> responseBody = Map.of(
                "data", data
        );

        stubFor(post("/")
                .withHeader(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE))
                .withRequestBody(equalToJson(requestBody))
                .willReturn(
                        aResponse()
                                .withStatus(HttpStatus.OK.value())
                                .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                                .withBody(objectMapper.writeValueAsString(responseBody))
                )
        );

        assertThat(graphQlClientService.dataCarHistoryByVin(vin).block())
                .usingRecursiveComparison()
                .isEqualTo(data.getDataPackage());

        WireMock.verify(postRequestedFor(urlEqualTo("/")));

    }

    @Test
    void dataCarHistoryByVin_shouldThrownException() throws JsonProcessingException {

        ...

    }

}

Тест полного прогона логики я продемонстрировал, остальное можно увидеть в вложенном проекте. Для тестового контекста сервиса необходимо передать все переменные, необходимые для конфигурации, данные требуемые для аутентификации и подготовить тестовый компонент. Это будет wiremock. Для него требуется отдельная библиотека, которая указана в pom сервиса. В самом тесте достаточно добавить аннотацию @AutoConfigureWireMock. Для того, чтобы подготовить интеграционный тест, использовать подготовленный запрос и ответ оказалось недостаточно. Понадобилось почистить данные. Предполагаю, что это ограничения/детали wiremock. Если вы знаете, почему, пишите в комментариях, будет полезно. В остальном wiremock отработал замечательно. Тестом мы полностью проверили наш клиент.

Вместо завершения и ссылка на репозиторий

На этом все, часть сервиса о которой мы хотели поговорить закончена и может использоваться. Мы создали клиент к Graph Ql, пользуясь рекомендованным spring boot компонентами. Проект готов. На этом примере я показал, что клиент к Graph Ql создаст дополнительный технический слой в приложении, но не усложнит логику самого приложения. Буду ждать комментариев. Спасибо команде. Вы поставляете задачи, которые помогают развиваться и улучшать нашу профессиональную жизнь, приносить больше пользы клиентам. До встречи.

Ссылка на репозиторий

Репозиторий с обезличенным проектом.