# Crear el proyecto Spring Boot


## Configurar el proyecto

Crea un proyecto Spring Boot desde [Spring Initializr](https://start.spring.io) seleccionando los iniciadores (starters) de **Spring Web**, **Validation**, **JDBC API**, **PostgreSQL Driver**, **Spring Security**, **OAuth2 Resource Server** y **Testcontainers**.

Alternativamente, puedes clonar el [repositorio de la guía](https://github.com/testcontainers/tc-guide-securing-spring-boot-microservice-using-keycloak-and-testcontainers).

Después de generar la aplicación, añade el módulo comunitario [testcontainers-keycloak](https://github.com/dasniko/testcontainers-keycloak) y [REST Assured](https://rest-assured.io/) como dependencias de prueba.

Las dependencias clave en `pom.xml` son:

```xml
<properties>
    <java.version>17</java.version>
    <testcontainers.version>2.0.4</testcontainers.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-testcontainers</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers-postgresql</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.github.dasniko</groupId>
        <artifactId>testcontainers-keycloak</artifactId>
        <version>3.4.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
```

## Crear el modelo de dominio

Crea un record `Product` que represente el objeto de dominio:

```java
package com.testcontainers.products.domain;

import jakarta.validation.constraints.NotEmpty;

public record Product(Long id, @NotEmpty String title, String description) {}
```

## Crear el repositorio

Implementa `ProductRepository` utilizando `JdbcClient` de Spring para interactuar con una base de datos PostgreSQL:

```java
package com.testcontainers.products.domain;

import java.util.List;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

@Repository
public class ProductRepository {

  private final JdbcClient jdbcClient;

  public ProductRepository(JdbcClient jdbcClient) {
    this.jdbcClient = jdbcClient;
  }

  public List<Product> getAll() {
    return jdbcClient.sql("SELECT * FROM products").query(Product.class).list();
  }

  public Product create(Product product) {
    String sql =
      "INSERT INTO products(title, description) VALUES (:title,:description) RETURNING id";
    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcClient
      .sql(sql)
      .param("title", product.title())
      .param("description", product.description())
      .update(keyHolder);
    Long id = keyHolder.getKeyAs(Long.class);
    return new Product(id, product.title(), product.description());
  }
}
```

## Añadir un script de creación de esquema

Crea `src/main/resources/schema.sql` para inicializar la tabla `products`:

```sql
CREATE TABLE products (
    id bigserial primary key,
    title varchar not null,
    description text
);
```

Habilita la inicialización del esquema en `src/main/resources/application.properties`:

```properties
spring.sql.init.mode=always
```

Para aplicaciones en producción, utiliza en su lugar una herramienta de migración de base de datos como Flyway o Liquibase.

## Implementar los endpoints de la API

Crea `ProductController` con los endpoints para obtener todos los productos y crear un producto:

```java
package com.testcontainers.products.api;

import com.testcontainers.products.domain.Product;
import com.testcontainers.products.domain.ProductRepository;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
class ProductController {

  private final ProductRepository productRepository;

  ProductController(ProductRepository productRepository) {
    this.productRepository = productRepository;
  }

  @GetMapping
  List<Product> getAll() {
    return productRepository.getAll();
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  Product createProduct(@RequestBody @Valid Product product) {
    return productRepository.create(product);
  }
}
```

## Configurar la seguridad OAuth 2.0

Crea una clase `SecurityConfig` que proteja los endpoints de la API mediante autenticación basada en tokens JWT:

```java
package com.testcontainers.products.config;

import static org.springframework.security.config.Customizer.withDefaults;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
class SecurityConfig {

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(c ->
        c
          .requestMatchers(HttpMethod.GET, "/api/products")
          .permitAll()
          .requestMatchers(HttpMethod.POST, "/api/products")
          .authenticated()
          .anyRequest()
          .authenticated()
      )
      .sessionManagement(c ->
        c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
      )
      .cors(CorsConfigurer::disable)
      .csrf(CsrfConfigurer::disable)
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
    return http.build();
  }
}
```

Esta configuración:

- Permite el acceso no autenticado a `GET /api/products`.
- Requiere autenticación para `POST /api/products` y todos los demás endpoints.
- Configura el servidor de recursos (Resource Server) OAuth 2.0 con autenticación basada en tokens JWT.
- Deshabilita CORS y CSRF debido a que esta es una API sin estado (stateless).

Añade la URI del emisor de JWT a `application.properties`:

```properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9090/realms/keycloaktcdemo
```

## Exportar la configuración del reino (realm) de Keycloak

Antes de escribir las pruebas, exporta una configuración del reino (realm) de Keycloak para que el entorno de prueba pueda importarla automáticamente. Inicia una instancia temporal de Keycloak:

```console
$ docker run -p 9090:8080 \
    -e KEYCLOAK_ADMIN=admin \
    -e KEYCLOAK_ADMIN_PASSWORD=admin \
    quay.io/keycloak/keycloak:25 start-dev
```

Abre `http://localhost:9090` e inicia sesión en la Consola de Administración con `admin/admin`. Luego, configura el reino (realm):

1. En la esquina superior izquierda, selecciona el menú desplegable de reinos y crea un reino llamado `keycloaktcdemo`.
2. Bajo el reino `keycloaktcdemo`, crea un cliente con la siguiente configuración:
   - **Client ID**: `product-service`
   - **Client Authentication**: **On**
   - **Authentication flow**: selecciona únicamente **Service accounts roles**
3. En la pantalla **Client details**, ve a la pestaña **Credentials** y copia el valor de **Client secret**.

Exporta la configuración del reino (realm):

```console
$ docker ps
# copia el ID del contenedor de keycloak

$ docker exec -it <container-id> /bin/bash

$ /opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --realm keycloaktcdemo

$ exit

$ docker cp <container-id>:/opt/keycloak/data/import/keycloaktcdemo-realm.json keycloaktcdemo-realm.json
```

Copia el archivo `keycloaktcdemo-realm.json` exportado en `src/test/resources`.

