Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,86 @@ http://localhost:8084

- На UI отображается:
**«Перевод выполнен...»**

---

# Контрактные тесты между accounts-service и transfer-service

В проекте настроены контрактные тесты Spring Cloud Contract для взаимодействия между сервисами **accounts-service** (провайдер API) и **transfer-service** (клиент этого API).

## Продюсер: accounts-service

Для сервиса `accounts-service` контракты описаны в `src/contractTest/resources/contracts/accounts`.

Пример контракта для эндпоинта `GET /accounts/ACC-001/owner`:

```groovy
Contract.make {
description 'Get owner of account ACC-001'
name 'get_owner_by_id'

request {
method GET()
url '/accounts/ACC-001/owner'
headers {
header 'Authorization', value(
consumer(regex('Bearer\s+.+')),
producer('Bearer test-token')
)
}
}

response {
status OK()
headers {
contentType(applicationJson())
}
body(
accountId: 'ACC-001',
ownerUsername: 'test-user'
)
}
}
```

Базовый тест `BaseAccountsContractTest` поднимает Spring Boot‑контекст, настраивает `MockMvc` и мок `AccountService`, чтобы контрактные тесты можно было прогонять без подключения к реальной базе данных.
Через отдельную конфигурацию `JwtTestConfig` переопределяется `JwtDecoder`, чтобы сервис принимал тестовый JWT с ролью `SERVICE`.

При сборке модуля `accounts-service`:

```bash
./gradlew :accounts-service:clean :accounts-service:build
```

Spring Cloud Contract:

- генерирует тесты по контрактам;
- выполняет их на стороне провайдера;
- собирает jar со стабами с classifier `stubs` (артефакт `ru.practicum:accounts-service:…:stubs`).

Этот jar со стабами необходимо выложить в Maven‑репозиторий.

## Консьюмер: transfer-service

В модуле `transfer-service` для проверки клиента `AccountsClient` используется тест `AccountsClientContractTest`.

В нем `StubRunner` поднимает локальный HTTP‑сервер на порту `8085` и отвечает по контрактам, загруженным из jar‑файла стабов `accounts-service`.
Тест вызывает реальный `AccountsClient` и проверяет, что он правильно формирует запрос к `accounts-service` и корректно обрабатывает ответ.

Важно: чтобы этот тест прошёл, jar со стабами `accounts-service` должен быть доступен в локальном Maven‑репозитории, откуда его заберёт Stub Runner.

## Как запустить контрактные тесты целиком

1. Собрать и опубликовать стабы `accounts-service` в локальный Maven‑репозиторий:

```bash
./gradlew :accounts-service:clean :accounts-service:build :accounts-service:publishToMavenLocal
```

(команда `publishToMavenLocal` предполагает, что в проекте настроен `maven-publish` и публикация стабов в `mavenLocal()` включена в конфигурацию Spring Cloud Contract).

2. Запустить контрактные тесты клиента в `transfer-service`:

```bash
./gradlew :transfer-service:test --tests '*AccountsClientContractTest'
```
26 changes: 26 additions & 0 deletions accounts-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
plugins {
alias(libs.plugins.spring.cloud.contract)
`maven-publish`
}

dependencies {
implementation(libs.spring.boot.starter.web)

Expand All @@ -6,5 +11,26 @@ dependencies {

implementation(libs.spring.boot.starter.actuator)

testImplementation(libs.spring.security.test)
testImplementation(libs.spring.cloud.starter.contract.verifier)
testImplementation(libs.spring.boot.starter.test)
}

contracts {
baseClassForTests.set(
"ru.practicum.accounts.service.contract.BaseAccountsContractTest"
)
}

publishing {
publications {
create<MavenPublication>("mavenJava") {
groupId = "ru.practicum"
artifactId = "accounts-service"
version = "0.0.1-SNAPSHOT"

from(components["java"])
artifact(tasks.named("verifierStubsJar"))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ru.practicum.accounts.service.contract;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import ru.practicum.accounts.service.model.Account;
import ru.practicum.accounts.service.service.AccountService;

import static org.mockito.Mockito.when;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("contract-test")
public abstract class BaseAccountsContractTest {

@Autowired
protected MockMvc mockMvc;

@MockitoBean
protected AccountService accountService;

@BeforeEach
void setup() {
RestAssuredMockMvc.mockMvc(mockMvc);

when(accountService.getAccount("ACC-001"))
.thenReturn(new Account("ACC-001", "test-user", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.practicum.accounts.service.contract;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;

import java.time.Instant;
import java.util.List;
import java.util.Map;

@Configuration
@Profile("contract-test")
public class JwtTestConfig {

@Bean
@Primary
public JwtDecoder jwtDecoder() {
return token -> {
Instant now = Instant.now();

return Jwt.withTokenValue(token)
.header("alg", "none")
.subject("contract-test")
.claim("realm_access", Map.of(
"roles", List.of("SERVICE")
))
.issuedAt(now)
.expiresAt(now.plusSeconds(3600))
.build();
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package contracts.accounts

import org.springframework.cloud.contract.spec.Contract

Contract.make {
description 'Get owner of account ACC-001'
name 'get_owner_by_id'

request {
method GET()
url '/accounts/ACC-001/owner'
headers {
header 'Authorization', value(
consumer(regex('Bearer\\s+.+')), // для консьюмера (WireMock): любой Bearer-токен
producer('Bearer test-token') // для провайдера (MockMvc-тест): ровно этот токен
)
}
}

response {
status OK()
headers {
contentType(applicationJson())
}
body(
accountId: 'ACC-001',
ownerUsername: 'test-user'
)
}
}
2 changes: 1 addition & 1 deletion bank-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ dependencies {
implementation(libs.spring.boot.starter.webflux)

testImplementation(libs.spring.boot.starter.test)
}
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-

spring-cloud-starter-gateway-server-webflux = { module = "org.springframework.cloud:spring-cloud-starter-gateway-server-webflux", version.ref = "contract-verifier" }

spring-cloud-starter-contract-verifier = { module = "org.springframework.cloud:spring-cloud-starter-contract-verifier", version.ref = "contract-verifier" }
spring-cloud-starter-contract-stub-runner = { module = "org.springframework.cloud:spring-cloud-starter-contract-stub-runner", version.ref = "contract-verifier" }

spring-security-test = { module = "org.springframework.security:spring-security-test", version.ref = "spring-security" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }

Expand Down
1 change: 1 addition & 0 deletions transfer-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dependencies {
implementation(libs.spring.boot.starter.actuator)

testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.spring.cloud.starter.contract.stub.runner)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.practicum.transfer.service.contract;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.ActiveProfiles;
import ru.practicum.transfer.service.client.AccountsClient;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("contract-test")
@AutoConfigureStubRunner(
ids = "ru.practicum:accounts-service:+:stubs:8085",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
class AccountsClientContractTest {

@Autowired
private AccountsClient accountsClient;

@Test
void shouldMatchContractWhenCheckingOwner() {
boolean result = accountsClient.isOwner("ACC-001", "test-user");

assertTrue(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
spring:
main:
allow-bean-definition-overriding: true

bank:
accounts-service:
base-url: http://localhost:8085