OPS-68: Implementation (#4)

* Implementation

* Readme + fixes

* Tests

* 🔄 Synced file(s) with valitydev/configurations (#3)

* 🔄 Created local '.github/workflows/basic-linters.yml' from remote 'workflows/base/basic-linters.yml'

* 🔄 Created local 'LICENSE' from remote 'LICENSE'

* 🔄 Created local '.github/settings.yml' from remote '.github/settings.yml'

Co-authored-by: Egor Cherniak <cherniak3@yandex.ru>

* Add renovate

* Feedback edits

Co-authored-by: Vality Bot <bots@vality.dev>
This commit is contained in:
Egor Cherniak 2022-05-31 18:30:04 +03:00 committed by GitHub
parent 2b25f77bd6
commit 1d169a7577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 3014 additions and 0 deletions

2
.github/settings.yml vendored Normal file
View File

@ -0,0 +1,2 @@
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
_extends: .github

10
.github/workflows/basic-linters.yml vendored Normal file
View File

@ -0,0 +1,10 @@
name: Vality basic linters
on:
pull_request:
branches:
- "*"
jobs:
lint:
uses: valitydev/base-workflows/.github/workflows/basic-linters.yml@v1

10
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,10 @@
name: Build Maven Artifact
on:
pull_request:
branches:
- '*'
jobs:
build:
uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v1

13
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Maven Deploy Artifact
on:
push:
branches:
- 'master'
jobs:
build-and-deploy:
uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v1
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }}

2
.github/workflows/settings.yml vendored Normal file
View File

@ -0,0 +1,2 @@
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
_extends: .github

79
.gitignore vendored Normal file
View File

@ -0,0 +1,79 @@
# Created by .ignore support plugin (hsz.mobi)
.eunit
deps
*.o
*.beam
*.plt
erl_crash.dump
ebin/*.beam
rel/example_project
.concrete/DEV_MODE
.rebar
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.DS_Store
.idea/
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
*.iws
*.ipr
*.iml
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
env.list

176
LICENSE Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1 +1,71 @@
# beholder # beholder
Сервис собирает метрики о производительности загрузки платёжной формы из разных регионов.
## Особенности имплементации
Точка входа в приложение: ```dev.vality.beholder.service.BeholderService.behold```. Этот метод вызывается по расписанию, указанном в свойстве ```schedule.cron```.
### Подготовка данных для загрузки платёжной формы
Для загрузки платёжной формы необходимы ```InvoiceId``` и ```InvoiceAccessToken```.
Схема взаимодействия с [swag-payments](https://github.com/valitydev/swag-payments):
![PaymentsImage](img/payments.drawio.svg)
Алгоритм взаимодействия реализован здесь: ```dev.vality.beholder.service.PaymentsService.prepareFormData```
### Загрузка платёжной формы
Beholder умеет работать c простым selenium-hub и с [lambdatest](https://www.lambdatest.com/).
В реальности обе интеграции работают через selenium API и являются совместимыми.
Алгоритм загрузки и сбора метрик формы реализован здесь: ```dev.vality.beholder.service.SeleniumService.executePaymentRequest```
Его можно разбить на следующие шаги:
1. Установить подключение с selenium-hub/lambdatest
2. Отправить запрос на загрузку формы
3. Собрать метрики загрузки формы посредством javascript'а (```dev.vality.beholder.util.SeleniumUtil.PERFORMANCE_SCRIPT```)
4. Заполнить форму и отправить запрос на проведение платежа
5. Собрать логи производительности браузера
### Обновление метрик
Собранная на предыдущем шаге информация о производительности формы записывается в соответствующие метрики prometheus'а.
Этот функционал реализован в классе ```dev.vality.beholder.service.MetricsService```
#### Метрики
| Название | Лейблы | Описание |
|--------------------------------------------------|-----------------|------------------------------------------------------------------------------|
| beholder_form_loading_requests_total | browser, region | счетчик запросов на загрузку формы |
| beholder_form_loading_failed_total | browser, region | счетчик неудачных загрузок формы |
| beholder_form_dom_complete_duration_millis | browser, region | время от момента отправки запроса до полной загрузки формы в миллисекундах |
| beholder_form_waiting_response_duration_millis | browser, region | время от момента отправки запроса до начала получения ответа в миллисекундах |
| beholder_form_receiving_response_duration_millis | browser, region | время между получением первым и последним байтом информации в миллисекундах |
| beholder_form_resource_loading_duration_millis | browser, region | время, затраченное на загрузку ресурса (включая блокировки, ожидание и т.д) |
## Тестирование
Поскольку загружать во время юнит-тестирования реальную платежную форму не представляется возможным,
реализован интеграционный тест, который отключен по умолчанию, однако может использоваться для локальной отладки сервиса.
Тест: ```dev.vality.beholder.IntegrationTest```
Подготовка к запуску теста:
1. Прописать валидные значения в следующих свойствах:
1. payments.api-url - адрес для обращения к api
2. payments.form-url - адрес для загрузки платёжной формы
3. payments.request.shop-id - идентификатор магазина, который нужно использовать
2. Прописать в свойстве ```dev.vality.beholder.IntegrationTest.TEST_USER_TOKEN``` валидный токен
3. Готово, можно запускать тест.
## Полезные ссылки
[Описание метрик производительности](https://developer.mozilla.org/en-US/docs/Web/Performance/Navigation_and_resource_timings), которые можно получить через JS.
[Описание метрик сети](https://chromedevtools.github.io/devtools-protocol/tot/Network/), которые можно получить от chromium'а.
[Регионы](https://www.lambdatest.com/capabilities-generator/), доступные для тестирования. На их основе заполнен справочник ```regions.json```

9
img/payments.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

204
pom.xml Normal file
View File

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.vality</groupId>
<artifactId>service-parent-pom</artifactId>
<version>1.0.16</version>
</parent>
<artifactId>beholder</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>beholder</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>15</java.version>
<server.port>8022</server.port>
<management.port>8023</management.port>
<exposed.ports>${server.port} ${management.port}</exposed.ports>
</properties>
<dependencies>
<!--vality-->
<dependency>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-thrift</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>shared-resources</artifactId>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
<artifactId>swag-payments</artifactId>
<version>1.627-0089567-client</version>
</dependency>
<!--spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--third party-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>15.1.1</version>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core-java11</artifactId>
<version>6.1.6</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<targetPath>${project.build.directory}</targetPath>
<includes>
<include>Dockerfile</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>Dockerfile</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-remote-resources-plugin</artifactId>
<version>1.7.0</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-filtering</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
<configuration>
<resourceBundles>
<resourceBundle>dev.vality:shared-resources:${shared-resources.version}</resourceBundle>
</resourceBundles>
<attachToMain>false</attachToMain>
<attachToTest>false</attachToTest>
</configuration>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

4
renovate.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>valitydev/.github:renovate-config"]
}

View File

@ -0,0 +1,17 @@
package dev.vality.beholder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@ServletComponentScan
@SpringBootApplication
@EnableScheduling
public class BeholderApplication extends SpringApplication {
public static void main(String[] args) {
SpringApplication.run(BeholderApplication.class, args);
}
}

View File

@ -0,0 +1,73 @@
package dev.vality.beholder.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import dev.vality.beholder.config.properties.PaymentsProperties;
import dev.vality.swag.payments.ApiClient;
import dev.vality.swag.payments.api.ClaimsApi;
import dev.vality.swag.payments.api.InvoicesApi;
import dev.vality.swag.payments.api.PartiesApi;
import dev.vality.swag.payments.api.ShopsApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.text.SimpleDateFormat;
@Configuration
public class PaymentsConfig {
private static final String BEARER_TYPE = "Bearer";
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(PaymentsProperties paymentsProperties) {
var requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(paymentsProperties.getApiTimeoutSec().intValue() * 1000);
return requestFactory;
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
var restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.getMessageConverters()
.removeIf(m -> m.getClass().getName().equals(MappingJackson2HttpMessageConverter.class.getName()));
Jackson2ObjectMapperBuilder builder =
new Jackson2ObjectMapperBuilder()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.dateFormat(new SimpleDateFormat(DATE_TIME_PATTERN));
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter(builder.build()));
return restTemplate;
}
@Bean
public ApiClient apiClient(RestTemplate restTemplate, PaymentsProperties paymentsProperties) {
ApiClient apiClient = new ApiClient(restTemplate);
apiClient.setBasePath(paymentsProperties.getApiUrl());
apiClient.setApiKeyPrefix(BEARER_TYPE);
return apiClient;
}
@Bean
public PartiesApi partiesApi(ApiClient apiClient) {
return new PartiesApi(apiClient);
}
@Bean
public ClaimsApi claimsApi(ApiClient apiClient) {
return new ClaimsApi(apiClient);
}
@Bean
public ShopsApi shopsApi(ApiClient apiClient) {
return new ShopsApi(apiClient);
}
@Bean
public InvoicesApi invoicesApi(ApiClient apiClient) {
return new InvoicesApi(apiClient);
}
}

View File

@ -0,0 +1,68 @@
package dev.vality.beholder.config;
import dev.vality.beholder.config.properties.PaymentsProperties;
import dev.vality.beholder.config.properties.SeleniumProperties;
import dev.vality.beholder.converter.LogEntriesToNetworkLogsConverter;
import dev.vality.beholder.model.Region;
import dev.vality.beholder.service.SeleniumService;
import dev.vality.beholder.util.FileUtil;
import dev.vality.beholder.util.SeleniumUtil;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
public class SeleniumConfig {
@Value("${dictionary.regions}")
private Resource dictionaryRegions;
@Bean(name = "remoteFormService")
@ConditionalOnProperty(name = "selenium.use-external-provider", havingValue = "true")
public SeleniumService lambdaTestRemoteFormService(SeleniumProperties seleniumProperties,
PaymentsProperties paymentsProperties,
LogEntriesToNetworkLogsConverter converter)
throws MalformedURLException {
URL lambdaTestUrl = buildLambdaTestUrl(seleniumProperties.getLambdaTest());
String formUrl = paymentsProperties.getFormUrl();
Long formTimeoutSec = paymentsProperties.getFormTimeoutSec();
DesiredCapabilities desiredCapabilities = SeleniumUtil.getLambdaTestCapabilities();
return new SeleniumService(lambdaTestUrl, formUrl, formTimeoutSec, desiredCapabilities, converter);
}
@Bean(name = "remoteFormService")
@ConditionalOnProperty(name = "selenium.use-external-provider", havingValue = "false")
public SeleniumService seleniumRemoteFormService(SeleniumProperties seleniumProperties,
PaymentsProperties paymentsProperties,
LogEntriesToNetworkLogsConverter converter)
throws MalformedURLException {
URL seleniumUrl = new URL(seleniumProperties.getUrl() + ":" + seleniumProperties.getPort());
String formUrl = paymentsProperties.getFormUrl();
Long formTimeoutSec = paymentsProperties.getFormTimeoutSec();
DesiredCapabilities desiredCapabilities = SeleniumUtil.getCommonCapabilities();
return new SeleniumService(seleniumUrl, formUrl, formTimeoutSec, desiredCapabilities, converter);
}
@Bean
public List<Region> regions(SeleniumProperties seleniumProperties) throws IOException {
return FileUtil.readRegions(dictionaryRegions)
.stream().filter(region -> seleniumProperties.getRegions().contains(region.getCode()))
.collect(Collectors.toList());
}
private URL buildLambdaTestUrl(SeleniumProperties.LambdaTestProperties lambdaTestProperties)
throws MalformedURLException {
String url = "https://" + lambdaTestProperties.getUser() + ":" + lambdaTestProperties.getToken() +
"@hub.lambdatest.com/wd/hub";
return new URL(url);
}
}

View File

@ -0,0 +1,30 @@
package dev.vality.beholder.config.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter
@Component
@Validated
@ConfigurationProperties(prefix = "keycloak")
public class KeycloakProperties {
@NotEmpty
private String user;
@NotEmpty
private String password;
@NotEmpty
private String url;
@NotEmpty
private String resource;
}

View File

@ -0,0 +1,49 @@
package dev.vality.beholder.config.properties;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@Component
@Validated
@ConfigurationProperties(prefix = "payments")
public class PaymentsProperties {
@NotEmpty
private String apiUrl;
private Long apiTimeoutSec = 10L;
@NotEmpty
private String formUrl;
private Long formTimeoutSec = 30L;
@NotNull
private Request request;
@Data
public static class Request {
@NotNull
private String shopId;
private Boolean createShopIfNotFound = false;
private Integer paymentInstitutionId;
private Integer categoryId;
}
@AssertTrue(message = "Check 'create-shop-if-not-found' option and related parameters")
private boolean isRequestConfigurationValid() {
return !request.createShopIfNotFound
|| (request.getPaymentInstitutionId() != null && request.getCategoryId() != null);
}
}

View File

@ -0,0 +1,49 @@
package dev.vality.beholder.config.properties;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Getter
@Setter
@Component
@Validated
@ConfigurationProperties(prefix = "selenium")
public class SeleniumProperties {
private String url;
private Integer port;
@NotNull
private Boolean useExternalProvider;
@NotEmpty
private List<String> regions;
private LambdaTestProperties lambdaTest;
@Data
public static class LambdaTestProperties {
private String user;
private String token;
}
@AssertTrue(message = "Check 'use-external-provider' option and related parameters")
private boolean isSeleniumConfigurationValid() {
return useExternalProvider && lambdaTest != null && !ObjectUtils.isEmpty(lambdaTest.token)
&& !ObjectUtils.isEmpty(lambdaTest.user)
|| !useExternalProvider && !ObjectUtils.isEmpty(url);
}
}

View File

@ -0,0 +1,46 @@
package dev.vality.beholder.converter;
import dev.vality.beholder.exception.BadFormatException;
import dev.vality.beholder.model.NetworkLog;
import dev.vality.beholder.model.NetworkMethod;
import org.openqa.selenium.logging.LogEntry;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class LogEntriesToNetworkLogsConverter implements Converter<List<LogEntry>, List<NetworkLog>> {
@Override
public List<NetworkLog> convert(List<LogEntry> source) {
Map<String, NetworkLog> networkLogs = new HashMap<>();
for (LogEntry logEntry : source) {
try {
//Message format doc: https://chromedevtools.github.io/devtools-protocol/tot/Network/
JSONObject log = new JSONObject(logEntry.getMessage());
JSONObject message = log.getJSONObject("message");
JSONObject params = message.getJSONObject("params");
if (params.has("requestId")) {
String requestId = params.getString("requestId");
String method = message.getString("method");
if (NetworkMethod.REQUEST_WILL_BE_SENT.getValue().equals(method)) {
double time = params.getDouble("timestamp") * 1000;
String resource = params.getJSONObject("request").getString("url");
networkLogs.put(requestId, new NetworkLog(resource, time, null));
} else if (NetworkMethod.LOADING_FINISHED.getValue().equals(method)) {
double time = params.getDouble("timestamp") * 1000;
NetworkLog networkLog =
networkLogs.getOrDefault(requestId, new NetworkLog(requestId, null, null));
networkLog.setEnd(time);
}
}
} catch (JSONException e) {
throw new BadFormatException("Error during parsing network logs:", e);
}
}
return new ArrayList<>(networkLogs.values());
}
}

View File

@ -0,0 +1,16 @@
package dev.vality.beholder.exception;
public class BadFormatException extends RuntimeException {
public BadFormatException(String message) {
super(message);
}
public BadFormatException(Throwable throwable) {
super(throwable);
}
public BadFormatException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,13 @@
package dev.vality.beholder.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum Browser {
CHROME("chrome");
@Getter
private final String label;
}

View File

@ -0,0 +1,26 @@
package dev.vality.beholder.model;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
@Data
public class FormDataRequest {
private String invoiceId;
private String invoiceAccessToken;
private Card cardInfo;
@Data
@Builder
public static class Card {
@ToString.Exclude
private String pan;
private String expiration;
@ToString.Exclude
private String cvv;
}
}

View File

@ -0,0 +1,38 @@
package dev.vality.beholder.model;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class FormDataResponse {
private FormDataRequest request;
private FormPerformance formPerformance;
private Browser browser;
private Region region;
private List<NetworkLog> networkLogs;
private boolean failed;
@Data
@Builder
public static class FormPerformance {
private Double requestStartAt;
private Double responseStartAt;
private Double responseEndAt;
private Double domCompletedAt;
}
}

View File

@ -0,0 +1,16 @@
package dev.vality.beholder.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class NetworkLog {
private String resource;
private Double start;
private Double end;
}

View File

@ -0,0 +1,17 @@
package dev.vality.beholder.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum NetworkMethod {
REQUEST_WILL_BE_SENT("Network.requestWillBeSent"),
RESPONSE_RECEIVED("Network.responseReceived"),
LOADING_FINISHED("Network.loadingFinished");
@Getter
private final String value;
}

View File

@ -0,0 +1,16 @@
package dev.vality.beholder.model;
import lombok.Data;
@Data
public class Region {
private String code;
private String country;
@Override
public String toString() {
return country + "[" + code + "]";
}
}

View File

@ -0,0 +1,30 @@
package dev.vality.beholder.security;
import dev.vality.beholder.config.properties.KeycloakProperties;
import lombok.RequiredArgsConstructor;
import org.keycloak.OAuth2Constants;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@Service
@RequiredArgsConstructor
public class KeycloakService {
private final RestTemplate restTemplate;
private final KeycloakProperties keycloakProperties;
public String getUserToken() {
HttpEntity<MultiValueMap<String, String>> request =
new TokenRequest.Builder(keycloakProperties.getResource(), OAuth2Constants.PASSWORD)
.add("username", keycloakProperties.getUser())
.add("password", keycloakProperties.getPassword())
.build();
ResponseEntity<String> response =
restTemplate.postForEntity(keycloakProperties.getUrl(), request, String.class);
return response.getBody();
}
}

View File

@ -0,0 +1,36 @@
package dev.vality.beholder.security;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Collections;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TokenRequest {
public static class Builder {
MultiValueMap<String, String> data;
public Builder(String clientId, String grantType) {
data = new LinkedMultiValueMap<>();
data.put("client_id", Collections.singletonList(clientId));
data.put("grant_type", Collections.singletonList(grantType));
}
public Builder add(String key, String value) {
data.put(key, Collections.singletonList(value));
return this;
}
public HttpEntity<MultiValueMap<String, String>> build() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return new HttpEntity<>(data, headers);
}
}
}

View File

@ -0,0 +1,47 @@
package dev.vality.beholder.service;
import dev.vality.beholder.model.FormDataRequest;
import dev.vality.beholder.model.FormDataResponse;
import dev.vality.beholder.model.Region;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class BeholderService {
private final PaymentsService paymentsService;
private final SeleniumService seleniumService;
private final MetricsService metricsService;
private final List<Region> regions;
@Scheduled(cron = "${schedule.cron:-}")
public void behold() {
log.info("Start sending requests from {} regions", regions);
List<FormDataResponse> responses = new ArrayList<>();
for (Region region : regions) {
FormDataRequest request;
try {
log.debug("Preparing request for {} region", region.getCountry());
request = paymentsService.prepareFormData();
log.debug("Request for {} region successfully prepared", region.getCountry());
} catch (Exception e) {
log.error("Unable to prepare request for {}:", region.getCountry(), e);
continue;
}
FormDataResponse response = seleniumService.executePaymentRequest(request, region);
responses.add(response);
log.debug("Metrics for {} region saved", region.getCountry());
}
metricsService.updateMetrics(responses);
log.info("Finished processing requests from {} regions", regions);
}
}

View File

@ -0,0 +1,158 @@
package dev.vality.beholder.service;
import dev.vality.beholder.model.FormDataResponse;
import dev.vality.beholder.util.MetricUtil;
import io.micrometer.core.instrument.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toList;
@Service
@Slf4j
public class MetricsService {
private final MeterRegistry meterRegistry;
private final MultiGauge resourcesLoadingTimings;
private final MultiGauge formDataWaitingDurationGauges;
private final MultiGauge formDataReceivingDuration;
private final MultiGauge formDomCompleteDuration;
private final Map<String, Counter> formLoadingCounters;
private final Map<String, Counter> formLoadingFailedCounters;
public MetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.resourcesLoadingTimings = MultiGauge.builder("beholder_form_resource_loading_duration")
.description("Resources uploading time")
.baseUnit("millis")
.register(meterRegistry);
this.formDataWaitingDurationGauges = MultiGauge.builder("beholder_form_waiting_response_duration")
.description("Time between sending request and first received byte of data")
.baseUnit("millis")
.register(meterRegistry);
this.formDataReceivingDuration = MultiGauge.builder("beholder_form_receiving_response_duration")
.description("Time between receiving first and last byte of data")
.baseUnit("millis")
.register(meterRegistry);
this.formDomCompleteDuration = MultiGauge.builder("beholder_form_dom_complete_duration")
.description("Time between sending request and fully rendered DOM")
.baseUnit("millis")
.register(meterRegistry);
this.formLoadingCounters = new HashMap<>();
this.formLoadingFailedCounters = new HashMap<>();
}
public void updateMetrics(List<FormDataResponse> formDataResponses) {
log.debug("Updating beholder metrics started");
updateWaitingResponseDuration(formDataResponses);
updateFormDataReceivingDuration(formDataResponses);
updateFormDomCompleteDuration(formDataResponses);
updateResourceLoadingDuration(formDataResponses);
updateFormLoadingRequestsTotal(formDataResponses);
updateFormLoadingFailedRequestsTotal(formDataResponses);
log.debug("Updating beholder metrics finished");
}
private void updateWaitingResponseDuration(List<FormDataResponse> formDataResponses) {
formDataWaitingDurationGauges.register(
formDataResponses.stream()
.filter(Predicate.not(FormDataResponse::isFailed))
.map(formDataResponse -> MultiGauge.Row.of(
MetricUtil.createCommonTags(formDataResponse),
MetricUtil.calculateWaitingResponseDuration(formDataResponse.getFormPerformance())))
.collect(toList()),
true
);
}
private void updateFormDataReceivingDuration(List<FormDataResponse> formDataResponses) {
formDataReceivingDuration.register(
formDataResponses.stream()
.filter(Predicate.not(FormDataResponse::isFailed))
.map(formDataResponse -> MultiGauge.Row.of(
MetricUtil.createCommonTags(formDataResponse),
MetricUtil.calculateDataReceivingDuration(formDataResponse.getFormPerformance())))
.collect(toList()),
true
);
}
private void updateFormDomCompleteDuration(List<FormDataResponse> formDataResponses) {
formDomCompleteDuration.register(
formDataResponses.stream()
.filter(Predicate.not(FormDataResponse::isFailed))
.map(formDataResponse -> MultiGauge.Row.of(
MetricUtil.createCommonTags(formDataResponse),
MetricUtil.calculateDomCompleteDuration(formDataResponse.getFormPerformance())))
.collect(toList()),
true
);
}
private void updateFormLoadingRequestsTotal(List<FormDataResponse> formDataResponses) {
for (FormDataResponse response : formDataResponses) {
String id = MetricUtil.getCounterId(response);
Counter counter = formLoadingCounters.getOrDefault(id,
Counter.builder("beholder_form_loading_requests")
.description("Total requests for form upload")
.tags(MetricUtil.createCommonTags(response))
.baseUnit("total")
.register(meterRegistry));
counter.increment();
formLoadingCounters.put(id, counter);
}
}
private void updateFormLoadingFailedRequestsTotal(List<FormDataResponse> formDataResponses) {
for (FormDataResponse response : formDataResponses) {
if (response.isFailed()) {
String id = MetricUtil.getCounterId(response);
Counter counter = formLoadingFailedCounters.getOrDefault(id,
Counter.builder("beholder_form_loading_failed")
.description("Total failed requests for form upload")
.tags(MetricUtil.createCommonTags(response))
.baseUnit("total")
.register(meterRegistry));
counter.increment();
formLoadingFailedCounters.put(id, counter);
}
}
}
private void updateResourceLoadingDuration(List<FormDataResponse> formDataResponses) {
formDataResponses.stream()
.filter(Predicate.not(FormDataResponse::isFailed))
.forEach(
formDataResponse ->
resourcesLoadingTimings.register(
formDataResponse.getNetworkLogs().stream()
.map(networkLog -> MultiGauge.Row.of(
MetricUtil.createCommonTags(formDataResponse)
.and("resource",
MetricUtil.getNormalisedPath(networkLog,
formDataResponse.getRequest()
.getInvoiceId(),
formDataResponse.getRequest()
.getInvoiceAccessToken())),
MetricUtil.calculateRequestDuration(networkLog)))
.collect(toList()),
true
)
);
}
}

View File

@ -0,0 +1,77 @@
package dev.vality.beholder.service;
import dev.vality.beholder.config.properties.PaymentsProperties;
import dev.vality.beholder.model.FormDataRequest;
import dev.vality.beholder.security.KeycloakService;
import dev.vality.beholder.util.PaymentsUtil;
import dev.vality.swag.payments.ApiClient;
import dev.vality.swag.payments.api.ClaimsApi;
import dev.vality.swag.payments.api.InvoicesApi;
import dev.vality.swag.payments.api.PartiesApi;
import dev.vality.swag.payments.api.ShopsApi;
import dev.vality.swag.payments.model.*;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
@Service
@RequiredArgsConstructor
public class PaymentsService {
private final ApiClient apiClient;
private final PartiesApi partiesApi;
private final ShopsApi shopsApi;
private final InvoicesApi invoicesApi;
private final ClaimsApi claimsApi;
private final KeycloakService keycloakService;
private final PaymentsProperties paymentsProperties;
public FormDataRequest prepareFormData() {
apiClient.setApiKey(keycloakService.getUserToken());
String shopId = getShopId();
InvoiceParams invoiceParams = PaymentsUtil.createInvoiceParams(shopId);
InvoiceAndToken invoiceAndToken = invoicesApi.createInvoice(PaymentsUtil.getRequestId(), invoiceParams,
PaymentsUtil.getRequestDeadline(paymentsProperties.getApiTimeoutSec()));
return createFormDataRequest(invoiceAndToken);
}
private String getShopId() {
Party party = partiesApi.getMyParty(PaymentsUtil.getRequestId(),
PaymentsUtil.getRequestDeadline(paymentsProperties.getApiTimeoutSec()));
var request = paymentsProperties.getRequest();
String shopId = request.getShopId();
try {
shopsApi.getShopByIDForParty(PaymentsUtil.getRequestId(), shopId, party.getId(),
PaymentsUtil.getRequestDeadline(paymentsProperties.getApiTimeoutSec()));
} catch (HttpStatusCodeException httpStatusCodeException) {
if (!isNotFoundError(httpStatusCodeException) || !request.getCreateShopIfNotFound()) {
throw httpStatusCodeException;
}
sendShopCreationClaim(request, shopId);
}
return shopId;
}
private void sendShopCreationClaim(PaymentsProperties.Request request, String shopId) {
ClaimChangeset changeset = PaymentsUtil.buildCreateShopClaim(request.getPaymentInstitutionId(),
shopId, request.getCategoryId());
claimsApi.createClaim(PaymentsUtil.getRequestId(), changeset,
PaymentsUtil.getRequestDeadline(paymentsProperties.getApiTimeoutSec()));
}
private FormDataRequest createFormDataRequest(InvoiceAndToken invoiceAndToken) {
FormDataRequest formDataRequest = new FormDataRequest();
formDataRequest.setCardInfo(FormDataRequest.Card.builder()
.pan(PaymentsUtil.TEST_CARD_PAN)
.cvv(PaymentsUtil.TEST_CARD_CVV)
.expiration(PaymentsUtil.TEST_CARD_EXPIRATION).build());
formDataRequest.setInvoiceId(invoiceAndToken.getInvoice().getId());
formDataRequest.setInvoiceAccessToken(invoiceAndToken.getInvoiceAccessToken().getPayload());
return formDataRequest;
}
private boolean isNotFoundError(HttpStatusCodeException httpStatusCodeException) {
return HttpStatus.NOT_FOUND.equals(httpStatusCodeException.getStatusCode());
}
}

View File

@ -0,0 +1,118 @@
package dev.vality.beholder.service;
import dev.vality.beholder.converter.LogEntriesToNetworkLogsConverter;
import dev.vality.beholder.model.*;
import dev.vality.beholder.util.SeleniumUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RequiredArgsConstructor
public class SeleniumService {
private final URL seleniumUrl;
private final String formUrl;
private final Long formTimeoutSec;
private final DesiredCapabilities desiredCapabilities;
private final LogEntriesToNetworkLogsConverter logEntriesToNetworkLogsConverter;
@Getter
private final Browser browser = Browser.CHROME;
public FormDataResponse executePaymentRequest(FormDataRequest formDataRequest, Region region) {
DesiredCapabilities capabilities = new DesiredCapabilities(desiredCapabilities);
updateCapabilities(capabilities, region);
RemoteWebDriver driver = null;
try {
driver = new RemoteWebDriver(seleniumUrl, capabilities);
driver.get(prepareParams(formDataRequest));
//noinspection rawtypes
ArrayList performanceMetrics = (ArrayList) driver.executeScript(SeleniumUtil.PERFORMANCE_SCRIPT);
fillAndSendPaymentRequest(driver, formDataRequest.getCardInfo());
LogEntries les = driver.manage().logs().get(LogType.PERFORMANCE);
List<NetworkLog> networkLogs = logEntriesToNetworkLogsConverter.convert(les.getAll());
return FormDataResponse.builder()
.networkLogs(networkLogs)
.request(formDataRequest)
.formPerformance(
FormDataResponse.FormPerformance.builder()
.requestStartAt(castToDouble(performanceMetrics.get(0)))
.responseStartAt(castToDouble(performanceMetrics.get(1)))
.responseEndAt(castToDouble(performanceMetrics.get(2)))
.domCompletedAt(castToDouble(performanceMetrics.get(3)))
.build()
)
.region(region)
.browser(browser)
.build();
} catch (Exception e) {
log.error("Error during sending request from {}:", region.getCountry(), e);
} finally {
if (driver != null) {
driver.quit();
}
}
return FormDataResponse.builder()
.region(region)
.browser(browser)
.failed(true)
.build();
}
private void updateCapabilities(DesiredCapabilities capabilities, Region region) {
capabilities.setCapability("browser", browser.getLabel());
capabilities.setCapability("geoLocation", region.getCode());
capabilities.setCapability("name", String.format("Payment from %s", region.getCountry()));
}
private void fillAndSendPaymentRequest(RemoteWebDriver driver, FormDataRequest.Card card) {
WebElement cardNumInput = new WebDriverWait(driver, formTimeoutSec).until(
ExpectedConditions.visibilityOfElementLocated(By.ById.id("card-number-input")));
cardNumInput.sendKeys(card.getPan());
driver.findElementById("expire-date-input").sendKeys(card.getExpiration());
driver.findElementById("secure-code-input").sendKeys(card.getCvv());
driver.findElementById("card-holder-input").sendKeys("Ivan Ivanov");
driver.findElementById("email-input").sendKeys("test@test.com");
driver.findElementById("pay-btn").click();
new WebDriverWait(driver, formTimeoutSec).until(
ExpectedConditions.visibilityOfElementLocated(By.ById.id("success-icon")));
}
private Double castToDouble(Object object) {
if (object instanceof Double) {
return (Double) object;
} else if (object instanceof Long) {
return ((Long) object).doubleValue();
} else {
log.warn("Unable to cast {} to double", object);
}
return Double.NaN;
}
private String prepareParams(FormDataRequest formDataRequest) {
return UriComponentsBuilder.fromHttpUrl(formUrl)
.queryParam("invoiceAccessToken", formDataRequest.getInvoiceAccessToken())
.queryParam("invoiceID", formDataRequest.getInvoiceId())
.build()
.toString();
}
}

View File

@ -0,0 +1,22 @@
package dev.vality.beholder.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.vality.beholder.model.Region;
import lombok.experimental.UtilityClass;
import org.springframework.core.io.Resource;
import java.io.*;
import java.util.List;
@UtilityClass
public class FileUtil {
public static List<Region> readRegions(Resource resource) throws IOException {
final ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(
resource.getFile(),
new TypeReference<>() {
});
}
}

View File

@ -0,0 +1,58 @@
package dev.vality.beholder.util;
import dev.vality.beholder.model.FormDataResponse;
import dev.vality.beholder.model.NetworkLog;
import io.micrometer.core.instrument.Tags;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@UtilityClass
public class MetricUtil {
public static String getNormalisedPath(NetworkLog networkLog, String invoiceId, String invoiceToken) {
String path = networkLog.getResource();
path = path.replaceAll(invoiceId, "{invoice_id}")
.replaceAll(invoiceToken, "{invoice_token}");
if (path.contains("nocache=")) {
path = path.substring(0, path.length() - 13);
}
return path;
}
public static double calculateRequestDuration(NetworkLog networkLog) {
return calculateDiff(networkLog.getStart(), networkLog.getEnd());
}
public static double calculateWaitingResponseDuration(FormDataResponse.FormPerformance performance) {
return calculateDiff(performance.getRequestStartAt(), performance.getResponseStartAt());
}
public static double calculateDataReceivingDuration(FormDataResponse.FormPerformance performance) {
return calculateDiff(performance.getResponseStartAt(), performance.getResponseEndAt());
}
public static double calculateDomCompleteDuration(FormDataResponse.FormPerformance performance) {
return calculateDiff(performance.getRequestStartAt(), performance.getDomCompletedAt());
}
private static double calculateDiff(Double from, Double to) {
if (isNumber(from) && isNumber(to)) {
return Math.round(to - from);
}
return 0.0;
}
private static boolean isNumber(Double value) {
return value != null && !value.isNaN();
}
public Tags createCommonTags(FormDataResponse formDataResponse) {
return Tags.of("browser", formDataResponse.getBrowser().getLabel(),
"region", formDataResponse.getRegion().getCode());
}
public String getCounterId(FormDataResponse formDataResponse) {
return String.join("_", formDataResponse.getBrowser().name(), formDataResponse.getRegion().getCountry());
}
}

View File

@ -0,0 +1,136 @@
package dev.vality.beholder.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.vality.swag.payments.model.*;
import lombok.experimental.UtilityClass;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@UtilityClass
public class PaymentsUtil {
public static final String TEST_CARD_PAN = "4242424242424242";
public static final String TEST_CARD_EXPIRATION = "12/24";
public static final String TEST_CARD_CVV = "123";
private static final ObjectMapper MAPPER = new ObjectMapper();
public static final String CURRENCY_CODE = "RUB";
public static PartyModification createShopModification(String shopId, String contractId, String payoutToolId) {
return new ShopCreation()
.details(
new ShopDetails()
.name("OOOBlackMaster")
.description("Goods for education"))
.location(
new ShopLocationUrl()
.url("http://all-time-favourite-spinners.com/"))
.contractID(contractId)
.payoutToolID(payoutToolId)
.shopID(shopId)
.shopModificationType(ShopModification.ShopModificationTypeEnum.SHOPCREATION)
.partyModificationType(PartyModification.PartyModificationTypeEnum.SHOPMODIFICATION);
}
public static PartyModification createShopAccountCreationModification(String shopId) {
return new ShopAccountCreation()
.currency(CURRENCY_CODE)
.shopID(shopId)
.shopModificationType(ShopModification.ShopModificationTypeEnum.SHOPACCOUNTCREATION)
.partyModificationType(PartyModification.PartyModificationTypeEnum.SHOPMODIFICATION);
}
public static PartyModification createShopCategoryChangeModification(String shopId, Integer categoryId) {
return new ShopCategoryChange()
.categoryID(categoryId)
.shopID(shopId)
.shopModificationType(ShopModification.ShopModificationTypeEnum.SHOPCATEGORYCHANGE)
.partyModificationType(PartyModification.PartyModificationTypeEnum.SHOPMODIFICATION);
}
public static ClaimChangeset buildCreateShopClaim(Integer paymentInstitutionId, String shopId, Integer categoryId) {
PartyModification contractCreation = createContractModification(shopId, paymentInstitutionId);
PartyModification contractModification = createPayoutToolModification(shopId);
PartyModification shopModification = createShopModification(shopId, shopId, shopId);
PartyModification categoryChange = createShopCategoryChangeModification(shopId, categoryId);
PartyModification accountChange = createShopAccountCreationModification(shopId);
ClaimChangeset changeset = new ClaimChangeset();
changeset.add(contractCreation);
changeset.add(contractModification);
changeset.add(shopModification);
changeset.add(categoryChange);
changeset.add(accountChange);
return changeset;
}
public static PartyModification createContractModification(String contractId, Integer paymentInstitutionId) {
return new ContractCreation()
.contractor(createContractor())
.paymentInstitutionID(paymentInstitutionId)
.contractID(contractId)
.contractModificationType(ContractModification.ContractModificationTypeEnum.CONTRACTCREATION)
.partyModificationType(PartyModification.PartyModificationTypeEnum.CONTRACTMODIFICATION);
}
public static Contractor createContractor() {
return new RussianLegalEntity()
.registeredName("testRegisteredName")
.registeredNumber("1234567890123")
.inn("1234567890")
.actualAddress("testActualAddress")
.postAddress("testPostAddress")
.representativePosition("testRepresentativePosition")
.representativeFullName("testRepresentativeFullName")
.representativeDocument("testRepresentativeDocument")
.bankAccount(createBankAccount())
.entityType(LegalEntity.EntityTypeEnum.RUSSIANLEGALENTITY)
.contractorType(Contractor.ContractorTypeEnum.LEGALENTITY);
}
public static PartyModification createPayoutToolModification(String contractId) {
return new ContractPayoutToolCreation()
.payoutToolID(contractId)
.currency(CURRENCY_CODE)
.details(new PayoutToolDetailsBankAccount()
.account("12345678901234567890")
.bankName("testBankName")
.bankPostAccount("12345678901234567890")
.bankBik("123456789")
)
.contractID(contractId)
.contractModificationType(ContractModification.ContractModificationTypeEnum.CONTRACTPAYOUTTOOLCREATION)
.partyModificationType(PartyModification.PartyModificationTypeEnum.CONTRACTMODIFICATION);
}
public static BankAccount createBankAccount() {
return new BankAccount()
.account("12345678901234567890")
.bankName("testBankName")
.bankPostAccount("12345678901234567890")
.bankBik("123456789");
}
public static InvoiceParams createInvoiceParams(String shopId) {
return new InvoiceParams()
.shopID(shopId)
.dueDate(OffsetDateTime.now().plusDays(1))
.currency(CURRENCY_CODE)
.product("Order num 12345")
.amount(1000L)
.metadata(MAPPER.createObjectNode());
}
public static String getRequestId() {
return RandomUtil.generateString(8);
}
public static String getRequestDeadline(long seconds) {
return ZonedDateTime.now()
.plusSeconds(seconds)
.format(DateTimeFormatter.ISO_INSTANT);
}
}

View File

@ -0,0 +1,22 @@
package dev.vality.beholder.util;
import lombok.experimental.UtilityClass;
import java.util.Random;
@UtilityClass
public class RandomUtil {
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final Random STRING_RANDOM = new Random();
public String generateString(int length) {
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
builder.append(ALPHABET.charAt(STRING_RANDOM.nextInt(ALPHABET.length())));
}
return builder.toString();
}
}

View File

@ -0,0 +1,40 @@
package dev.vality.beholder.util;
import lombok.experimental.UtilityClass;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.util.logging.Level;
@UtilityClass
public class SeleniumUtil {
public static final String PERFORMANCE_SCRIPT = """
var data = new Array();
var navigation = window.performance.getEntriesByType("navigation")[0];
data[0] = navigation.requestStart;
data[1] = navigation.responseStart;
data[2] = navigation.responseEnd;
data[3] = navigation.domComplete;
return data;""";
public static DesiredCapabilities getCommonCapabilities() {
var capabilities = new DesiredCapabilities();
capabilities.setCapability("build", "Simple payment test");
var logPrefs = new LoggingPreferences();
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
capabilities.setCapability(CapabilityType.LOGGING_PREFS, logPrefs);
return capabilities;
}
public static DesiredCapabilities getLambdaTestCapabilities() {
var capabilities = getCommonCapabilities();
capabilities.setCapability("network", false);
capabilities.setCapability("visual", false);
capabilities.setCapability("video", false);
capabilities.setCapability("console", false);
return capabilities;
}
}

View File

@ -0,0 +1,62 @@
server:
port: '@server.port@'
management:
server:
port: '@management.port@'
metrics:
tags:
application: '@project.name@'
export:
prometheus:
enabled: true
endpoint:
health:
show-details: always
metrics:
enabled: true
prometheus:
enabled: true
endpoints:
web:
exposure:
include: health,info,prometheus
spring:
application:
name: '@project.name@'
output:
ansi:
enabled: always
info:
version: '@project.version@'
stage: dev
logging:
level:
root: INFO
schedule:
cron: '0 */5 * * * *' #every 5 minutes
keycloak:
url: http://127.0.0.1
user: user
password: admin
resource: external
payments:
api-url: http://127.0.0.1
api-timeout-sec: 30
form-url: http://127.0.0.1
form-timeout-sec: 30
request:
shop-id: 1
create-shop-if-not-found: true
payment-institution-id: 1
category-id: 1
selenium:
url: http://127.0.0.1
port: 4444
use-external-provider: false
regions:
- BR
dictionary:
regions: classpath:regions.json

View File

@ -0,0 +1,140 @@
` `
`` ` ` ` `
` ` ,***,```
:*; `;*:````
:i` ` .i*```
`.*` ``..` :*``
`*`` `:#zz, `*,``
,; ```nx#i#` .+.`
:: ``nxz#+` `z,``
:;` `:#nz: ` +:.`
,*`````.,,` #;.`
.+,` `` `` `n;.`
.i;```` ```` i+:.`
`.z:. ` ` ``i#;:``
`.+#:,````:#+*:,`
``:*z+z#+Mn+i;,``
`.+;x@+n**;:,.`
`i;nzi#*::.``
``:;*z**;,.`
:iiiz;:.`
`;:,:#;,`
`*i;*+;.`
` ` ::;,i+:,` ` ` ` `` `
`````` `;:..:*:.` ` ` .`` `
`:*;;+i` ` ` `*,`.*+:.` ```i*;i*,` ``
`,;+;;+#i;*i.`` *,,`i**,. i*` ```ii. ` `.``
`:ii;:::*nM#;:i+; .i,``.ni.` ;:``+##. i;``:*;;;i*,``
` ```` ,++:```+nx; ` :i.````:i:`.i+:.` .i``ixxzz.`+i+;:;,,,`;*:`.::;i+z++i+i*+**i.`` `
`````ii:`.:;M#,` `` `i:`` `*i` .#i:.` i: `*xx#z,`,+;,.` ` ` `*#i:ii:++##zi.````:*.``
`` ` ` `:;:``#++M# ``..``,i.``,*;``i#;,` #, .+zx*.` z``` .````i;``,`.,;;nn`````` .*.```
` ` ``````:*. .**nM;```:z#i ,*,``*;, ,;#:.` `#. ``::````* ``++n+`*:````.i;+#x: ```````*,.``
`,:::::.```+;.`,:n#x: `:zzn#.:+,.,;+. `**:.` `+, ` ,;,:;:::;n, ``.:i;i;:i` ``.:,``.+.`
,*::,.,:i,``*;` ;*z+z, `inz*#.*+,.:ii, `+;,. ` ``#. ```` `*.i:####z:` .,+#####: ` ;#zz: `z:``
` ` `:i```` ```*,.*i` .,zi+:``:nxz;;zi,.+*i.``+;,` `,z:` ` `:+...x**i+:, `.*ii#ii;;#. `,zM*i#``z:.`
,i ``,**.`,#,i:i ``n;;+ ``:;:;z*;,;;*::``+;,` ,*;*:` .:iz. .i+i:;+:. `*. :#;,,,*, :zWz##``n:.`
i.` `;xn#n` i;ii#. :#;;zi;;i##*;,.+:*+*``+i,```.,;;i..+@@#i*i+```:#i;,+,, `;n.`.*;.`.i;` `*zxz:`*+:.`
` `.* ``.zn+,#:`;*:#:+ `:#:;*+##*i;..`++M#ii;++**ii;::. .,iz#:z*+: ,zi:;i:```x:z.`+:.``,+`` `:::`izi,.`
;.` .znWzn,`:*:;*ii` .*i:;;::,.`,i*,`` ` ` ` `,iz;in#:i ```+:;*,, +#ii. +:.```,z: ``.:+#i:.``
i` ` :#nn+` :*::*:*,` `,**,.,,.`i;...``` `` ` `,:*;;z#*#.i`` `#;+:: ``#++;, ;i.```.;*#####+i;,`
`i` ` ``````;+:.,#:;,```;**,.`.*i.`` ` ` ` ``,,:*;+z*i;*,z; z+.` ;#iz;i`.z,.` `.,;iiii;:,.`
` ,i ` ``````zi,..:#;+:`.`#;#..+*i:` `..,..,`,:+z+#+i;:,;:i. ` :+,` ` **;z:#..z:``` ..,::::,.``
``*: `` ,n#i,`..:#;*i.:,Mi##;+..``` `,:**ii++nx#**i;;:,...+,+.;+:`` .ni:#;n+:i+i,` ``.``.``
`:#, `.i*#z*:,``..:+;#x*,**z*,.` ..*i;:;;##*i;i:::,.````*in::` `` `+*;,*iWx;:;:i:`` ````
`.;*zznW+#n+i;,.` `.;z;#x++W:#..` `;:zi,.```,+*;,,...``.::i*, ` ` *#;:.*#;` ``` +,```
``:i+xxi+#*i;,.`` `*+ii**xxni+#.````,,x:;:.` `..+:,````:*;:,``` ` ` i#*:,`#,` `````;i```
`,;;;i;#*;:,```:ii.,,:;:izn:#:... `:inii:.:. `:i:.``:*: ..:` ` ` `,##i;,.`i` `:i;` :*.``
``+``.;#;:`.`;i:,+:,,:,,n*++:`..` .,++#,n,.` `..+,`*:`.:,.` ` ,,;+n*i:.`.i``;nnn*`:*,.`
`*:` .,#*ii+, `..,,`:.,,z:,#;.` ;:ni+:nz;.``i;z;i.:*i;.` `.;:#*;*,`#i:. `* `nMi*+`;+,.`
`:i,:` ``` ` ``;:,.,:,x:**i.. *:zi*;i+i:. ::+#.,+#;` `;,;;#i:` ;+,.` `# `+Mnn*`#i,.`` ` `
` `.i+ii,` ` ` `.;i#*n+zz*+,*i*.. `+:*ii;+ni.,,:i*,i#+::, `::;*n*zi` `,*,` `,*..*+*.*+i,` ` ` ` ```:;**;,`` ``
```.i#;;i;i,:i*z**:;ii;;#n:+i,:, .++;+;i+M#i;:ini,xi*;..,;:##++;z*`` ..#,.` `.*+;.,*z#i:.````` ``i:. ,;+.` `
` `.,;#+;;:ii;ii*znz++zz,#**i;+:,;*n;+;i+zi**#+#`inn;+*:;,n#*;*,xi. ` .*i,` ``,:*z#*ii:.`` ```.;*,```` `:*``
` `i++++#znn+++###+***i;;;i+*z:#+z,:#;++:ni#nnz+zx*`+nMnx:ii+z*###:x;.```,,+,.:i++#zz*;i:.`` .:*i;i:*.```,,```z..`
,i*.` `.:++#*;;;::,,,.,:i*W:M+,i:i*iz#;+M@Mz@#M:+WznWnz#:x@@z#;zx#,.,,;.n+i::::,.,;+;.``:*i.`:i;**``.#xz#` :*.`
` ;+,. ` `````i:*+:,,....```i;@+;ii:++:+Wn#;n@@xzzx#i;,.,;+M+zWxW#;M*i;;:;+.*;*;#****;;,;i,i;i` ``:ni``;xn:z,`.+,``
.;i,` `..` ` ``;,,*i,` ` ` `;i@@,zxWii#z+WWW#,. ``,` `` `:i+x*:zn*i:i;*#:##;:,::,```,`:#i+.```.,+@; `:nxnz, .z,``
`,i;` ..,;;,..`. `` ,;;i*;.`` `;z##+,xznzzMW@z:` ` . `.``.;M#zx+i:i*#.n+::,,:` ``++,``.*:;i#;``.:zz;``:#:``
i;,`;:izn#zzzi,:.`` ``:,;****izn@@W*;MxMMn:,` ` .,;:,i*:;;,:,` ` ``#WxMnWzz#.ni;:.`` ```` `,i.,;:;iiii;``````` `zi,.
.**`*.+z*ii;ii+#*:#i`` ``:.`,**+*#MMzi:+xi``. ```:;+nnMnx#M+#**:`` ``:xWx@nM:iz:::,:.`` `*i;:##+++#n````````;z;,``
`` ``**z#;+#i;:,,,::;*#:+#i``` `;:+#+#Wx#*: ` `.:,*zMMxxnnzznnxMWz*;,`` .MMWMz,nW++nn#ii;: ` *;;n*i;:;;i+. .ini:.`
`` `*+MMx;ni:,.```..,:i+#i#xi*:` .;;*Wzi.`.. ` ,**zzi;i*++**##*:;nWx#i,` `iWxn;*MzzzMi,,;,,, :+z*;:,..,,*z,,,+z#i;,.`
`` `.*zi;+xW*#;,.` ```,;in*i#+*:,..;;*##*``` . .`;;ixn+i+i` ``` ``;+;#+zWz:` `:W#,WMMzz,:x@nz#,.` :x*;:.````.,:+z#+ii;,.`
`` :*:` ```**+;.` ` .i+***+*#:i#zxi+nxz@` ``` `;z*MM,zi:` ` ``*``` ,i**@xz;;.,,#+MMM#.;W@@x*#;. `i+i:.`` `.,:;i;::.``
``,* `` ` ,+:` ,ii,;i*+#MxW#i;*zxxn@# ` .ii;#z#x*x: `` .zM+` ` *+:z@M**. :MWx+inM#W#n@M+: ` zi;.` ```...,.``
``*` ``` ``z:` i;. ``:**+zWxW@Wxz*:;n+ ;;M,nWz#W:` ``` #Mnx,` ` `;M*;#+#;.`:;#W@+i+###WWz*```#;,`` ` ````````
:; ``,,.` +;.` `:*` ```ii#;#xn####*x@@Mz `.iiMn.*MW;,` ` ` ;Mxznn. ` `.;Wz.M@i:,,`.x##*@zzW#@x, .,#;,` ``` `
::``+#z#, ;*.` +i` `**:*+M+x##@#x@MM#n``i+#@i+M@*:,. ```znnznxi`` `,;@z:x@+,,.`z+MzxWn*##ni..+:+,.:**+; ```
;,`;xx.+#` ,*.. ,*:` ``;#n#*++;zMnzWxz+i;:,+zW#+znW:,,, ``;nnnxnnn`` ,ixzM:W#zi: *W*i##Wx#n#,,`:i*+*:` .+:``
;:`;nx,+z. ,*,.`,++ ```z+***+zzi,;**i::*;;iixx*z+@#`;:.` ``+xnnMnzM``` ` .:#+n#;+i*``,n*iz#;;*nz;:`;M#+, `#.``
.:;``+MMn* ` i+:.``++;.`.*+;::;*iz#:.``,..;.i+i,*zMx:;.:.` ``znnx@xnx.`````.;,Mixi#@*,.,i#:`,i#*n+#,:;*#` ` ``` `+.`
`.#` .:ii`` `zi,``.iin+;;;*+,,,;+;zWxz*#:,`,#xn;xi@#+,``.` `MnnM@nnx` ````,:;xin+;*+:`:+` :+MiMWin:+#+;` ``.:. `i:``
.,z;``` ``:n*;.```,+innnM+,*+*i*x*;zxx*::`*+W:#ix@+,.:..`` `xxnMx;nx` `` .,i*M+M+:xW+;n;+xx:#M@z+#.#M` `+nz#``;*.`
`,;*+:.,*z+*i:.```.:+i@#,` `,#MWz#+*:i.,,;;;i#xM,,,;..` ``zxnM,:nn `` ;#i:Wzz*,#z+:,+#z:++#@#z++`z. ,xM*#:`;*,`
````,:i+##*ii;,.`` `.;++` ``` :@Wn#*i;:`,:i*,;i*#*;*,.,:.` *Mxxi#x+`````.i,,iMzii`:;;*;*z#z:*ix#z*i#,,` ;nMin:`;+,.
```.::;;;;:,.. ` ` `:#.` ````` WWzM++;ii;i+:.:zi#n*+##n+i:.;xWM@MMz:,,;iiz+nMnz;i*xW@n#;::i**#MM##i*z,. ,xnnz``;+,.
...,..``` `;:`` :nxx* xWn;*+@#xnn;.,#z+n+ii*+*;inzzi,..`.,:;:..:+x@z:;i,*nznznzznzz##nWW*zzz+.``.;i.` +i,`
``````` `;,` ,zn#ix. xW+;:#n*+W*,:;,+:;i;::,,:.`,.`.`,i;i##ii;*,.*i,.,::++#i+Mxz#zW;*#WMx###;```````:z;,`
`i,``:nx+*n.`xi+;#i*##. .`i, ``.:;;,,` ` `,`,;::,:..`` , ` ,``,*ii.,#z;+***n@Mz*+++,````*zi:``
`;:``:znMx#`,i+zi;i:;:````:. ```....,,:` ,;i,` ` `` ` ;.```iii.;:::z#+n@x*+;++zz#z##*:,``
`:i `*#z+,`#+n**#.**.,*`,, `` ` `` .,:*+nn,.` ` `:z` `i..ii*;;;*+zn*#n#*:;iii*ii;,```
.*: `..``+++;i+`+zi,*,`:` ` ` ``` `..,,*+*;:.` .```,zzxi :# .n;ii;i,+##x+i#;,::;::,,.`
..++::,,*M##.ii.`#M::i.., `i;i:;:. `` ` ` ` ````` ` ``` `i*` ;:.n:+,:*`;nx++n+i,.....```
`.:i++zzWn;;.i:..n#:.:`.` ::;.```` `..` ```. .`` ` ,` ``+ ` .`.:#,+::,i;nn+*;+;.````
``.,:;#xWx;* ::`.+x;,.` `` ` ., .,`. .. ` .. `+.` `:,`*@,z.;;z`nx+;:,..`
``,,+xxW#;.:+: *nz,*.,`,`. ` ,` ;ni .:,`:i;.`` :````#` ``,:`xn,+:;**`nni;,.``
```*zzxnMM:;`iM..+@:.,: ,;..;, ,.,#* iMx;zMxnzn+;:...: ::i;`; ``,nMi*`i.#*.n@;+,``
`;nz#zM*Mi;,.n#`+@,` :`.i:;`.;i+:+ni ix, .xMMi``x#z+nz;.`iii*.,``ni:,i,,*: z#n*;;;`
`;*;i++n,z+:: ;x:*W:` :#:;:.;xzx+ :#;`*M, `#@#; W@ xzzM#*:nz*i,+z;#x:,*:*`,Mx#M+:``
..,:;++,+ii+ .z+,M,;:`;i:inz#:M+`,#i iW, :@##; #x `@#@x`:+;;#zxz*nx:`#.+.`.:#nzi,.`
``.,*+:+i#;in;:x +,,*z+:+x:.i`nz i@n ;@; ###@; .#z :##@n `Mn*zn*niW::z:i#.,;.,xi;,`
` ``:;,x:.:n#ix,#,,#*nn*;*#:`# zz`+#@ ,@i n##@; :@z ##@#z `n*#xx#z#z.n,`z#,*iz`++:,`
` :;i*,:` :nx:*.ni**z#;+i`z`+`#W n##, @+`z###; i#*.x#### ii+*MM#:+:`i, in:i;*i,#:.`
` `i.;+nnn*`:M**,*#*W+Mz,+`*;i,*#`n### Mz`n###i z#;;@@##i`z:;xWni:#.:,:,i.``..*:i;.``
` `;::*iii+z#,*M::.n;xz@+z;i.#:ii#;+#@W;+M`n###*.xM,n###M::i;iMMz,**`i;i,`:+#+:.;:*;``
`***+n#M+n;z,*:*`**+z@+Wi#.+:z:@#i####i#`n###*:Wn.@##@n`z+:MMM+:+,:+*:`;Mx+;;::,,+.``
` `,:i;#x.zni+:+,,.#.zM+xW*n.n#:xx:####:x:z###+i#*+##@#;*+;n#xx,:z`#;# ,M+,n#+;:,`*:.`
`.,,;;+,n#z,.*.; *`*xzx#WWi*z#n@;##@##niz###++#:M##@W:Mn#@#n#.;z.*,#.in`+#n+z+++:i.`
`````*iz@MW;.,;,::,i##x###@;x@@@zn##@Wnnz###+Mzz###@*WW+@##n,;;z,+.z.#,z#x;n*i**n;,`
````+#*nW#x,`z`:;,;;#M###@x+##@@####@@x####z@n#@##@W@x@##@n`;iW;#;+;:#Wx;x+#;::n:.`
`.,+nx*M#;x.:#i.*;@####@xM@########@n###W####@#########W+,*Wi#x,,#+i,*#i:,..,..`
.,i;MW+M:x.+;n;i;####@#@@@##@###@##W#######@#####@#####+*#M#;x:#,:i;z*;:.``.```
``,**##n;W:;+zi;;####@#####@#@###@##########@###@##@###*ziMM*+x;.#z#*;,.`` `
`.:*MWx;x#;z+i*#######################@###@##@#####@@#iMizni*zi.+i#;:.`
``.+nMM:xx:#++#MW@######@#######################@@W##@izi*#nzn, ;n*:..`
``*nMM,zM:@*z#W+@##@##M@################@###@###W*##@n*i;+#M*,.,,#:. `
`**Mxi*x+n##zxi#@#@zW##########W@##########z@###*+##xzii#*@::Mz+;*.`
`nx+zn;+z+z,xn,####M########@W##W####@#WW##*@@###,W#@++#Mzni;+z+*;.``
`,;+xin:iWM,n+,W#@:#iz#######W#;W######x###ixxx#M`n#@z:znxn,#x#*;,.`
.,#+z*:;x#;+;,#@x,W:n######i#W:W######x;##innn#@ #iz#`W@nz:W*+;:.``
`.ii#*,n+#M,#i;@n.x.n#####W:#z,M######M.##;#x*##`i*n.+#zxiM##i:.``
``,+#:.+;@@#,*#nn`n`z#####n`@#.x######M,W#iix,##`iin.xWxMxni#;,`
``.:+,:::W#@+.+@# n`*#####n #*`z######M,n#i,x.@n i#z;MWWz++#i:.`
` `.+.;`+@###,iMi`n`i##@##z #i z####@#M,z#:`M,M+ i#.;WMMxxi#;,`
` `;iiinM##@M+,#zin`,x@###* @: *######M.+@``x.n+;M`.+@#M#W;+:,
`.;*#zzW##@#ii,@Mn` +@###:`x: :######n`#z `x`#iM+`i##WWW;+i:.
;i*+#x+M#W@z*i:#n, ;xx#@. #: .#####@# #i` Wn*#`,,i#@W@z**;.`
.,:;*+zz#W+zi#.`:zn+..;x +; `######,`++ ,+#i`;;#@@@@Mi#i:.`
``.,,:;++#W@#**+,` *n@#i#,`+i .xnnn,#.`*n*+*+ :*M@M@#@zn*;,``
``...,:*#+#@*nzi; .,;ix+i+zx#+.:,:i,##*+nn*i`i+#x@###*#i:.
` ```.,;z#MWi@#+zi,.``iz*Mn;;M@#@#@;:+x*..:i:W###xi+,;i,`
`.,;##zx+@x+++,;.,;,;+W@#M#*i*#zx: :;*ix+zW+*zz*z;.`
``.:iz*xn@#zii+i+:,.`::.``.````:`:+#nW#nniz#**i;,.`
`.,*ziM@@#xz:::ix,i**;i;zz++i;;#+n#Mn###*i;::,.`
`.,;xi#xxW#n,;,,*#+*+*i+;i,.i#z@#M**z*i;:,.```
``:#++##+#z;xz:zM##Mz#n+*, x#@@M*##ii:,.``
`,:;;*+zM;+#zz##++z@##+,;@@x++z*;:,.``
``,,,:;;zizz**#n@#@####.#z#++z*;:.``
```...,#;#+zznnzzz#++z:z#+#i*;,.``
``,*;#**iiiiiiii;+;+ii*+;,.
``*+;::::,,,,,,,i;*;::,,.`
`.,,,..........i:i,,...`
```...`` `+,*,.`````
``` `;,+,.``
` .ii+;.`
`**z:.`
``:::.`
..,.``
``````
` ```

View File

@ -0,0 +1,414 @@
[
{
"code": "AL",
"country": "Albania"
},
{
"code": "AD",
"country": "Andorra"
},
{
"code": "AR",
"country": "Argentina"
},
{
"code": "AM",
"country": "Armenia"
},
{
"code": "AU",
"country": "Australia"
},
{
"code": "AT",
"country": "Austria"
},
{
"code": "AZ",
"country": "Azerbaijan"
},
{
"code": "BH",
"country": "Bahrain"
},
{
"code": "BD",
"country": "Bangladesh"
},
{
"code": "BY",
"country": "Belarus"
},
{
"code": "BE",
"country": "Belgium"
},
{
"code": "BA",
"country": "Bosnia and Herzegovina"
},
{
"code": "BR",
"country": "Brazil"
},
{
"code": "BG",
"country": "Bulgaria"
},
{
"code": "KH",
"country": "Cambodia"
},
{
"code": "CA",
"country": "Canada"
},
{
"code": "CL",
"country": "Chile"
},
{
"code": "CN",
"country": "China"
},
{
"code": "CO",
"country": "Colombia"
},
{
"code": "CR",
"country": "Costa Rica"
},
{
"code": "HR",
"country": "Croatia"
},
{
"code": "CW",
"country": "Curacao"
},
{
"code": "CY",
"country": "Cyprus"
},
{
"code": "CZ",
"country": "Czech Republic"
},
{
"code": "DK",
"country": "Denmark"
},
{
"code": "DO",
"country": "Dominican Republic"
},
{
"code": "EC",
"country": "Ecuador"
},
{
"code": "EG",
"country": "Egypt"
},
{
"code": "SV",
"country": "El Salvador"
},
{
"code": "EE",
"country": "Estonia"
},
{
"code": "FI",
"country": "Finland"
},
{
"code": "FR",
"country": "France"
},
{
"code": "GE",
"country": "Georgia"
},
{
"code": "DE",
"country": "Germany"
},
{
"code": "GR",
"country": "Greece"
},
{
"code": "GT",
"country": "Guatemala"
},
{
"code": "HN",
"country": "Honduras"
},
{
"code": "HK",
"country": "Hong Kong"
},
{
"code": "HU",
"country": "Hungary"
},
{
"code": "IS",
"country": "Iceland"
},
{
"code": "IN",
"country": "India"
},
{
"code": "ID",
"country": "Indonesia"
},
{
"code": "IE",
"country": "Ireland"
},
{
"code": "IM",
"country": "Isle of Man"
},
{
"code": "IL",
"country": "Israel"
},
{
"code": "IT",
"country": "Italy"
},
{
"code": "JM",
"country": "Jamaica"
},
{
"code": "JP",
"country": "Japan"
},
{
"code": "JO",
"country": "Jordan"
},
{
"code": "KZ",
"country": "Kazakhstan"
},
{
"code": "KE",
"country": "Kenya"
},
{
"code": "KR",
"country": "Korea"
},
{
"code": "KW",
"country": "Kuwait"
},
{
"code": "KG",
"country": "Kyrgyzstan"
},
{
"code": "LV",
"country": "Latvia"
},
{
"code": "LB",
"country": "Lebanon"
},
{
"code": "LT",
"country": "Lithuania"
},
{
"code": "LU",
"country": "Luxembourg"
},
{
"code": "MY",
"country": "Malaysia"
},
{
"code": "MT",
"country": "Malta"
},
{
"code": "MX",
"country": "Mexico"
},
{
"code": "MD",
"country": "Moldova"
},
{
"code": "MN",
"country": "Mongolia"
},
{
"code": "MA",
"country": "Morocco"
},
{
"code": "NL",
"country": "Netherlands"
},
{
"code": "NZ",
"country": "New Zealand"
},
{
"code": "NI",
"country": "Nicaragua"
},
{
"code": "NG",
"country": "Nigeria"
},
{
"code": "NO",
"country": "Norway"
},
{
"code": "OM",
"country": "Oman"
},
{
"code": "PK",
"country": "Pakistan"
},
{
"code": "PA",
"country": "Panama"
},
{
"code": "PY",
"country": "Paraguay"
},
{
"code": "PE",
"country": "Peru"
},
{
"code": "PH",
"country": "Philippines"
},
{
"code": "PL",
"country": "Poland"
},
{
"code": "PT",
"country": "Portugal"
},
{
"code": "PR",
"country": "Puerto Rico"
},
{
"code": "QA",
"country": "Qatar"
},
{
"code": "RO",
"country": "Romania"
},
{
"code": "RU",
"country": "Russian Federation"
},
{
"code": "SA",
"country": "Saudi Arabia"
},
{
"code": "RS",
"country": "Serbia"
},
{
"code": "SG",
"country": "Singapore"
},
{
"code": "SK",
"country": "Slovakia"
},
{
"code": "SI",
"country": "Slovenia"
},
{
"code": "ZA",
"country": "South Africa"
},
{
"code": "ES",
"country": "Spain"
},
{
"code": "SE",
"country": "Sweden"
},
{
"code": "CH",
"country": "Switzerland"
},
{
"code": "TW",
"country": "Taiwan"
},
{
"code": "TZ",
"country": "Tanzania"
},
{
"code": "TH",
"country": "Thailand"
},
{
"code": "TN",
"country": "Tunisia"
},
{
"code": "TR",
"country": "Turkey"
},
{
"code": "UA",
"country": "Ukraine"
},
{
"code": "AE",
"country": "United Arab Emirates"
},
{
"code": "GB",
"country": "United Kingdom"
},
{
"code": "US",
"country": "United States"
},
{
"code": "UY",
"country": "Uruguay"
},
{
"code": "UZ",
"country": "Uzbekistan"
},
{
"code": "VE",
"country": "Venezuela"
},
{
"code": "VN",
"country": "Vietnam"
}
]

View File

@ -0,0 +1,193 @@
package dev.vality.beholder;
import dev.vality.beholder.config.properties.PaymentsProperties;
import dev.vality.beholder.config.properties.SeleniumProperties;
import dev.vality.beholder.security.KeycloakService;
import dev.vality.beholder.service.BeholderService;
import dev.vality.beholder.service.SeleniumService;
import dev.vality.beholder.testutil.ResponseUtil;
import dev.vality.swag.payments.ApiClient;
import dev.vality.swag.payments.api.ClaimsApi;
import dev.vality.swag.payments.api.InvoicesApi;
import dev.vality.swag.payments.api.PartiesApi;
import dev.vality.swag.payments.api.ShopsApi;
import dev.vality.swag.payments.model.Party;
import org.junit.jupiter.api.*;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.actuate.metrics.AutoConfigureMetrics;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.client.HttpClientErrorException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@AutoConfigureMetrics
@AutoConfigureMockMvc
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"beholder.cron=-", //disables scheduled execution
"payments.request.shop-id=test",
"payments.request.create-shop-if-not-found=true",
"payments.request.payment-institution-id=1",
"payments.request.category-id=1",
"management.server.port="})
public class BeholderFullTest {
public static final String TEST_USER_TOKEN = "test_token";
@MockBean
private KeycloakService keycloakService;
@MockBean
private ApiClient apiClient;
@MockBean
private PartiesApi partiesApi;
@MockBean
private ShopsApi shopsApi;
@MockBean
private InvoicesApi invoicesApi;
@MockBean
private ClaimsApi claimsApi;
@MockBean
private SeleniumService seleniumService;
@Autowired
public BeholderService beholderService;
@Autowired
public SeleniumProperties seleniumProperties;
@Autowired
public PaymentsProperties paymentsProperties;
@Autowired
private MockMvc mockMvc;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
public void init() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {keycloakService, apiClient, partiesApi, shopsApi, invoicesApi, claimsApi,
seleniumService};
}
@AfterEach
public void clean() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
public void beholdWithExistingShop() throws Exception {
Party party = ResponseUtil.getMyPartyResponse();
String shopId = paymentsProperties.getRequest().getShopId();
when(keycloakService.getUserToken()).thenReturn(TEST_USER_TOKEN);
when(partiesApi.getMyParty(anyString(), anyString())).thenReturn(party);
when(shopsApi.getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString()))
.thenReturn(ResponseUtil.getShopResponse(shopId));
when(invoicesApi.createInvoice(anyString(), any(), anyString()))
.thenReturn(ResponseUtil.getInvoiceAndToken());
when(seleniumService.executePaymentRequest(any(), any()))
.thenReturn(ResponseUtil.getFormDataResponse());
beholderService.behold();
verify(keycloakService, times(1)).getUserToken();
verify(apiClient, times(1)).setApiKey(TEST_USER_TOKEN);
verify(partiesApi, times(1)).getMyParty(anyString(), anyString());
verify(shopsApi, times(1)).getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString());
verify(invoicesApi, times(1)).createInvoice(anyString(), any(), anyString());
verify(seleniumService, times(1)).executePaymentRequest(any(), any());
var mvcResult = mockMvc.perform(get("/actuator/prometheus"))
.andReturn();
String prometheusResponse = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
List<String> metrics = Arrays.stream(prometheusResponse.split("\n"))
.filter(row -> row.startsWith("beholder_")).collect(Collectors.toList());
Assertions.assertFalse(metrics.isEmpty());
}
@Test
public void beholdWithShopCreation() throws Exception {
when(keycloakService.getUserToken()).thenReturn(TEST_USER_TOKEN);
Party party = ResponseUtil.getMyPartyResponse();
when(partiesApi.getMyParty(anyString(), anyString())).thenReturn(party);
String shopId = paymentsProperties.getRequest().getShopId();
when(shopsApi.getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString()))
.thenThrow(new HttpClientErrorException(HttpStatus.NOT_FOUND));
when(claimsApi.createClaim(anyString(), any(), anyString()))
.thenReturn(ResponseUtil.getClaim());
when(invoicesApi.createInvoice(anyString(), any(), anyString()))
.thenReturn(ResponseUtil.getInvoiceAndToken());
when(seleniumService.executePaymentRequest(any(), any()))
.thenReturn(ResponseUtil.getFormDataResponse());
beholderService.behold();
verify(keycloakService, times(1)).getUserToken();
verify(apiClient, times(1)).setApiKey(TEST_USER_TOKEN);
verify(partiesApi, times(1)).getMyParty(anyString(), anyString());
verify(shopsApi, times(1)).getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString());
verify(claimsApi, times(1)).createClaim(anyString(), any(), anyString());
verify(invoicesApi, times(1)).createInvoice(anyString(), any(), anyString());
verify(seleniumService, times(1)).executePaymentRequest(any(), any());
var mvcResult = mockMvc.perform(get("/actuator/prometheus"))
.andReturn();
String prometheusResponse = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
List<String> metrics = Arrays.stream(prometheusResponse.split("\n"))
.filter(row -> row.startsWith("beholder_")).collect(Collectors.toList());
Assertions.assertFalse(metrics.isEmpty());
}
@Test
public void beholdWithGetShopError() throws Exception {
when(keycloakService.getUserToken()).thenReturn(TEST_USER_TOKEN);
Party party = ResponseUtil.getMyPartyResponse();
when(partiesApi.getMyParty(anyString(), anyString())).thenReturn(party);
String shopId = paymentsProperties.getRequest().getShopId();
when(shopsApi.getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString()))
.thenThrow(new HttpClientErrorException(HttpStatus.I_AM_A_TEAPOT));
beholderService.behold();
verify(keycloakService, times(1)).getUserToken();
verify(apiClient, times(1)).setApiKey(TEST_USER_TOKEN);
verify(partiesApi, times(1)).getMyParty(anyString(), anyString());
verify(shopsApi, times(1)).getShopByIDForParty(anyString(), eq(shopId),
eq(party.getId()), anyString());
var mvcResult = mockMvc.perform(get("/actuator/prometheus"))
.andReturn();
String prometheusResponse = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
List<String> metrics = Arrays.stream(prometheusResponse.split("\n"))
.filter(row -> row.startsWith("beholder_")).collect(Collectors.toList());
Assertions.assertFalse(metrics.isEmpty());
}
}

View File

@ -0,0 +1,111 @@
package dev.vality.beholder;
import dev.vality.beholder.config.properties.SeleniumProperties;
import dev.vality.beholder.security.KeycloakService;
import dev.vality.beholder.service.BeholderService;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.actuate.metrics.AutoConfigureMetrics;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.utility.DockerImageName;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static dev.vality.beholder.testutil.SystemUtil.isArmArchitecture;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@Disabled("Used only for local testing")
@AutoConfigureMetrics
@AutoConfigureMockMvc
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"beholder.cron=-", //disables scheduled execution
"payments.api-url=https://api.test.com",
"payments.form-url=https://checkout.test.com/checkout.html",
"payments.request.shop-id=test", "management.server.port="})
public class IntegrationTest {
public static final String TEST_USER_TOKEN = "test_token";
public static final String SELENIUM_IMAGE_NAME =
(isArmArchitecture() ? "seleniarm" : "selenium") + "/standalone-chromium";
public static final String SELENIUM_IMAGE_TAG = "101.0";
@Container
public static final GenericContainer SELENIUM_CONTAINER = new GenericContainer(DockerImageName
.parse(SELENIUM_IMAGE_NAME)
.withTag(SELENIUM_IMAGE_TAG))
.withExposedPorts(4444)
.waitingFor(new HostPortWaitStrategy());
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
Supplier<Object> seleniumUrlSupplier = () -> "http://" + SELENIUM_CONTAINER.getHost();
Supplier<Object> seleniumPortSupplier = () -> SELENIUM_CONTAINER.getMappedPort(4444);
registry.add("selenium.url", seleniumUrlSupplier);
registry.add("selenium.port", seleniumPortSupplier);
}
@Autowired
public BeholderService beholderService;
@MockBean
private KeycloakService keycloakService;
@Autowired
public SeleniumProperties seleniumProperties;
@Autowired
private MockMvc mockMvc;
@BeforeAll
public static void prepare() {
SELENIUM_CONTAINER.setPortBindings(List.of("4444:4444"));
SELENIUM_CONTAINER.start();
}
@AfterEach
public void cleanUp() {
verifyNoMoreInteractions(keycloakService);
}
@AfterAll
public static void stop() {
SELENIUM_CONTAINER.stop();
}
@Test
public void test() throws Exception {
when(keycloakService.getUserToken()).thenReturn(TEST_USER_TOKEN);
beholderService.behold();
verify(keycloakService, times(1)).getUserToken();
var mvcResult = mockMvc.perform(get("/actuator/prometheus"))
.andReturn();
String prometheusResponse = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
List<String> metrics = Arrays.stream(prometheusResponse.split("\n"))
.filter(row -> row.startsWith("beholder_")).collect(Collectors.toList());
Assertions.assertFalse(metrics.isEmpty());
System.out.println("Collected metrics: ");
metrics.forEach(System.out::println);
}
}

View File

@ -0,0 +1,50 @@
package dev.vality.beholder.converter;
import dev.vality.beholder.exception.BadFormatException;
import dev.vality.beholder.model.NetworkLog;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.logging.LogEntry;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.time.Instant;
import java.util.List;
import java.util.logging.Level;
import static org.junit.jupiter.api.Assertions.*;
class LogEntriesToNetworkLogsConverterTest {
private static final LogEntriesToNetworkLogsConverter CONVERTER = new LogEntriesToNetworkLogsConverter();
@Test
void convertValidLogs() throws IOException {
String start = getMessage("valid_start_network_log.json");
String stop = getMessage("valid_finish_network_log.json");
LogEntry startLogEntry = new LogEntry(Level.ALL, Instant.now().toEpochMilli(), start);
LogEntry endLogEntry = new LogEntry(Level.ALL, Instant.now().toEpochMilli(), stop);
List<NetworkLog> converted = CONVERTER.convert(List.of(startLogEntry, endLogEntry));
assertNotNull(converted);
assertEquals(1, converted.size());
NetworkLog networkLog = converted.get(0);
assertNotNull(networkLog.getStart());
assertNotNull(networkLog.getEnd());
assertTrue(networkLog.getStart() < networkLog.getEnd());
assertEquals("https://checkout.test.com/v1/fonts/90d16760.woff2", networkLog.getResource());
}
@Test
void convertInvalidLog() throws IOException {
String invalid = getMessage("invalid_network_log.json");
LogEntry logEntry = new LogEntry(Level.ALL, Instant.now().toEpochMilli(), invalid);
assertThrows(BadFormatException.class, () -> CONVERTER.convert(List.of(logEntry)));
}
private String getMessage(String filePath) throws IOException {
var resource = new ClassPathResource(filePath, getClass());
return FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream()));
}
}

View File

@ -0,0 +1,77 @@
package dev.vality.beholder.service;
import dev.vality.beholder.model.FormDataRequest;
import dev.vality.beholder.model.FormDataResponse;
import dev.vality.beholder.model.Region;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.web.client.HttpClientErrorException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@SpringBootTest(properties = {"beholder.cron=-",
"selenium.regions=AL,AD,AR"})
class BeholderServiceTest {
@Autowired
BeholderService beholderService;
@Autowired
List<Region> regions;
@MockBean
PaymentsService paymentsService;
@MockBean
SeleniumService seleniumService;
@MockBean
MetricsService metricsService;
private AutoCloseable mocks;
private Object[] preparedMocks;
@BeforeEach
void setUp() {
mocks = MockitoAnnotations.openMocks(this);
preparedMocks = new Object[] {paymentsService, seleniumService, metricsService};
}
@AfterEach
void tearDown() throws Exception {
verifyNoMoreInteractions(preparedMocks);
mocks.close();
}
@Test
void behold() {
var firstRequest = new FormDataRequest();
var secondRequest = new FormDataRequest();
when(paymentsService.prepareFormData())
.thenReturn(new FormDataRequest())
.thenReturn(new FormDataRequest())
.thenThrow(HttpClientErrorException.class);
var firstResponse = FormDataResponse.builder().build();
var secondResponse = FormDataResponse.builder().build();
when(seleniumService.executePaymentRequest(firstRequest, regions.get(0)))
.thenReturn(firstResponse);
when(seleniumService.executePaymentRequest(secondRequest, regions.get(1)))
.thenReturn(secondResponse);
beholderService.behold();
verify(paymentsService, times(3)).prepareFormData();
verify(seleniumService, times(2)).executePaymentRequest(any(), any());
verify(metricsService, times(1)).updateMetrics(List.of(firstResponse, secondResponse));
}
}

View File

@ -0,0 +1,78 @@
package dev.vality.beholder.testutil;
import dev.vality.beholder.model.Browser;
import dev.vality.beholder.model.FormDataRequest;
import dev.vality.beholder.model.FormDataResponse;
import dev.vality.beholder.model.Region;
import dev.vality.swag.payments.model.*;
import lombok.experimental.UtilityClass;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
@UtilityClass
public class ResponseUtil {
public static Party getMyPartyResponse() {
Party party = new Party();
party.setId(UUID.randomUUID().toString());
party.setIsBlocked(false);
party.setIsBlocked(false);
return party;
}
public static Shop getShopResponse(String id) {
Shop shop = new Shop();
shop.setId(id);
shop.setCurrency("RUB");
shop.setIsBlocked(false);
shop.setIsSuspended(false);
return shop;
}
public static Claim getClaim() {
Claim claim = new Claim();
claim.setId(0L);
claim.setStatus("success");
return claim;
}
public static InvoiceAndToken getInvoiceAndToken() {
InvoiceAndToken invoiceAndToken = new InvoiceAndToken();
invoiceAndToken.setInvoice(new Invoice().id("2"));
invoiceAndToken.setInvoiceAccessToken(new AccessToken().payload("invoice_test_token"));
return invoiceAndToken;
}
public static FormDataResponse getFormDataResponse() {
var region = new Region();
region.setCode("AM");
region.setCountry("Armenia");
var invoiceAndToken = getInvoiceAndToken();
FormDataRequest request = new FormDataRequest();
request.setInvoiceAccessToken(invoiceAndToken.getInvoiceAccessToken().getPayload());
request.setInvoiceId(invoiceAndToken.getInvoice().getId());
var requestStartAt = (double) Instant.now().minus(10, ChronoUnit.SECONDS).toEpochMilli();
var responseStartAt = requestStartAt + 5;
var responseEndAt = responseStartAt + 5;
FormDataResponse.FormPerformance performance = FormDataResponse.FormPerformance.builder()
.requestStartAt(requestStartAt)
.responseStartAt(responseStartAt)
.responseEndAt(responseEndAt).build();
return FormDataResponse.builder()
.region(region)
.browser(Browser.CHROME)
.request(request)
.networkLogs(List.of())
.formPerformance(performance)
.build();
}
}

View File

@ -0,0 +1,21 @@
package dev.vality.beholder.testutil;
import lombok.experimental.UtilityClass;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.HardwareAbstractionLayer;
@UtilityClass
public class SystemUtil {
private static final String ARM = "ARM";
public static boolean isArmArchitecture() {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
CentralProcessor cpu = hal.getProcessor();
var cpuId = cpu.getProcessorIdentifier();
return cpuId.getVendor().toUpperCase().contains(ARM)
|| cpuId.getMicroarchitecture().toUpperCase().contains(ARM);
}
}

View File

@ -0,0 +1,12 @@
{
"msg": {
"method": "Network.loadingFinished",
"params": {
"encodedDataLength": 65958,
"requestId": "325.11",
"shouldReportCorbBlocking": false,
"timestamp": 30884.97731
}
},
"webview": "142FDA4010363970DE1DED0689086616"
}

View File

@ -0,0 +1,12 @@
{
"message": {
"method": "Network.loadingFinished",
"params": {
"encodedDataLength": 65958,
"requestId": "325.11",
"shouldReportCorbBlocking": false,
"timestamp": 30884.97731
}
},
"webview": "142FDA4010363970DE1DED0689086616"
}

View File

@ -0,0 +1,37 @@
{
"message": {
"method": "Network.requestWillBeSent",
"params": {
"documentURL": "https://checkout.test.com/v1/checkout.html?test_token=sample_token",
"frameId": "142FDA4010363970DE1DED0689086616",
"hasUserGesture": false,
"initiator": {
"type": "parser",
"url": "https://checkout.test.com/v1/aeee2b0f52d9e4fffb4e.css"
},
"loaderId": "A7EB9BE4EDAD0AFA88AB6E16CA26ECAD",
"redirectHasExtraInfo": false,
"request": {
"headers": {
"Origin": "https://checkout.test.com",
"Referer": "https://checkout.test.com/v1/aeee2b0f52d9e4fffb4e.css",
"User-Agent": "Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36",
"sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\""
},
"initialPriority": "VeryHigh",
"isSameSite": true,
"method": "GET",
"mixedContentType": "none",
"referrerPolicy": "strict-origin-when-cross-origin",
"url": "https://checkout.test.com/v1/fonts/90d16760.woff2"
},
"requestId": "325.11",
"timestamp": 30884.700929,
"type": "Font",
"wallTime": 1653919988.713246
}
},
"webview": "142FDA4010363970DE1DED0689086616"
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="warn">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="dev.vality.woody" level="ALL"/>
</configuration>