chore(ci): Align GH Workflows with Docker SSoT, new paths; minimal SSoT guard; staticAnalysis (#23)

* chore(MP-21): snapshot pre-refactor state (Epic 1)

* chore(MP-22): scaffold new repo structure, relocate Docker Compose, move frontend/backend modules, update Makefile; add docs mapping and env template

* MP-22 Epic 2: Erfolgreich umgesetzt und verifiziert

* MP-23 Epic 3: Gradle/Build Governance zentralisieren

* MP-23 Epic 3: Gradle/Build Governance zentralisieren

* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt

- ENV Single Source of Truth
  - docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
  - config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])

- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
  - Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
    und Start mit -c config_file=/etc/postgresql/postgresql.conf
  - Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
    und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
  - Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
  - Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT

- Frontend/DI/Network (MP-23 Grundlage)
  - :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
  - Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
  - Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)

- Build/Gradle & Module-Refs
  - settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
  - Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
  - Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht

- Dockerfiles angepasst
  - backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
  - backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service

- Static Analysis / Guards
  - config/detekt/detekt.yml hinzugefügt
  - Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet

- Doku
  - docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
  - docs/adr/README.md angelegt

BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)

Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`

Refs: MP-22 (Epic 2), MP-23 (Epic 3)

* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt

- ENV Single Source of Truth
  - docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
  - config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])

- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
  - Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
    und Start mit -c config_file=/etc/postgresql/postgresql.conf
  - Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
    und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
  - Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
  - Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT

- Frontend/DI/Network (MP-23 Grundlage)
  - :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
  - Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
  - Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)

- Build/Gradle & Module-Refs
  - settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
  - Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
  - Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht

- Dockerfiles angepasst
  - backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
  - backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service

- Static Analysis / Guards
  - config/detekt/detekt.yml hinzugefügt
  - Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet

- Doku
  - docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
  - docs/adr/README.md angelegt

BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)

Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`

Refs: MP-22 (Epic 2), MP-23 (Epic 3)

* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt

- ENV Single Source of Truth
  - docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
  - config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])

- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
  - Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
    und Start mit -c config_file=/etc/postgresql/postgresql.conf
  - Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
    und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
  - Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
  - Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT

- Frontend/DI/Network (MP-23 Grundlage)
  - :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
  - Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
  - Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)

- Build/Gradle & Module-Refs
  - settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
  - Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
  - Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht

- Dockerfiles angepasst
  - backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
  - backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service

- Static Analysis / Guards
  - config/detekt/detekt.yml hinzugefügt
  - Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet

- Doku
  - docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
  - docs/adr/README.md angelegt

BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)

Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`

Refs: MP-22 (Epic 2), MP-23 (Epic 3)

* chore(ci): Workflows an Docker-SSoT & neue Struktur angepasst, minimaler SSoT-Guard

- ssot-guard.yml: Option B (minimal) → `docker compose -f docker/docker-compose.yaml config` als Lint
- integration-tests.yml: `./gradlew staticAnalysis` vor Integrationstests
- docs-kdoc-sync.yml: Dokka-Task Fallback (dokkaGfmAll || dokkaGfm), YouTrack-Sync nur wenn Script vorhanden
- deploy-proxmox.yml: Compose-Pfade auf docker/docker-compose.yaml + `--env-file docker/.env`; Build/Test Schritte vereinheitlicht
- ci-main.yml: SSoT-Skripte per `if: hashFiles(...)` guarded, Compose-Lint Fallback; OpenAPI‑Pfad → backend/gateway; ADR‑Pfade → docs/adr/**; `staticAnalysis` in Build integriert
- youtrack-sync.yml: unverändert (funktional)

Refs: MP-22, MP-23

* chore(ci): Workflows an Docker-SSoT & neue Struktur angepasst, minimaler SSoT-Guard

- ssot-guard.yml: Option B (minimal) → `docker compose -f docker/docker-compose.yaml config` als Lint
- integration-tests.yml: `./gradlew staticAnalysis` vor Integrationstests
- docs-kdoc-sync.yml: Dokka-Task Fallback (dokkaGfmAll || dokkaGfm), YouTrack-Sync nur wenn Script vorhanden
- deploy-proxmox.yml: Compose-Pfade auf docker/docker-compose.yaml + `--env-file docker/.env`; Build/Test Schritte vereinheitlicht
- ci-main.yml: SSoT-Skripte per `if: hashFiles(...)` guarded, Compose-Lint Fallback; OpenAPI‑Pfad → backend/gateway; ADR‑Pfade → docs/adr/**; `staticAnalysis` in Build integriert
- youtrack-sync.yml: unverändert (funktional)

Refs: MP-22, MP-23

* fix(ci): create .env from example before validating compose config

* fix(ci): update ssot-guard filename (.yaml) and sync workflow state

* fixing

* fix(webpack): correct sql.js fallback configuration for webpack 5
This commit is contained in:
StefanMo
2025-12-03 12:03:40 +01:00
committed by GitHub
parent 034892e890
commit 95fe3e0573
365 changed files with 2283 additions and 15142 deletions
@@ -0,0 +1,31 @@
// Dieses Modul stellt die zentrale, wiederverwendbare Konfiguration
// für die Verbindung mit Apache Kafka bereit (z.B. Bootstrap-Server, Serializer).
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
}
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.bootJar {
enabled = false
}
// Stellt sicher, dass stattdessen ein reguläres Jar gebaut wird
tasks.jar {
enabled = true
}
dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
api(platform(projects.platform.platformBom))
// Stellt gemeinsame Abhängigkeiten bereit.
api(projects.platform.platformDependencies)
// OPTIMIERUNG: Verwendung des `kafka-config`-Bundles.
// `api` wird verwendet, damit der `messaging-client` diese Konfigurationen
// und Abhängigkeiten (wie Jackson) direkt nutzen kann.
api(libs.bundles.kafka.config)
// Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting)
}
@@ -0,0 +1,136 @@
package at.mocode.infrastructure.messaging.config
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.kafka.clients.producer.ProducerConfig
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.kafka.common.serialization.StringSerializer
import org.springframework.kafka.core.DefaultKafkaProducerFactory
import org.springframework.kafka.support.serializer.JsonDeserializer
import org.springframework.kafka.support.serializer.JsonSerializer
/**
* Zentrale Kafka-Konfiguration mit optimierten Einstellungen für Performance und Zuverlässigkeit.
*
* Diese Klasse kann programmatisch instanziiert werden (z. B. in Tests) oder
* als Spring-@Configuration mit @Bean-Methoden in einem Application Context registriert werden.
*
* Erweitert um Konfigurationsvalidierung und zusätzliche Optimierungen.
*/
class KafkaConfig {
/**
* Kommagetrennte Liste von host:port-Paaren für die initiale Verbindung zum Kafka-Cluster.
*/
var bootstrapServers: String = "localhost:9092"
set(value) {
require(value.isNotBlank()) { "Bootstrap servers cannot be blank" }
// Support both simple format (host:port) and protocol-prefixed format (PLAINTEXT://host:port)
val isValidFormat = value.matches(Regex("^[a-zA-Z0-9._-]+:[0-9]+(,[a-zA-Z0-9._-]+:[0-9]+)*$")) ||
value.matches(Regex("^[A-Z]+://[a-zA-Z0-9._-]+:[0-9]+(,[A-Z]+://[a-zA-Z0-9._-]+:[0-9]+)*$"))
require(isValidFormat) {
"Bootstrap servers must be in format 'host:port' or 'PROTOCOL://host:port'"
}
field = value
}
/**
* Standard-Präfix für Consumer-Group-IDs.
*/
var defaultGroupIdPrefix: String = "messaging-client"
set(value) {
require(value.isNotBlank()) { "Default group ID prefix cannot be blank" }
require(value.matches(Regex("^[a-zA-Z0-9._-]+$"))) {
"Group ID prefix must contain only alphanumeric characters, dots, underscores, and hyphens"
}
field = value
}
/**
* Comma-separated list of trusted packages for JSON deserialization security.
* Default restricts to application packages only.
*/
var trustedPackages: String = "at.mocode.*"
set(value) {
require(value.isNotBlank()) { "Trusted packages cannot be blank" }
field = value
}
/**
* Enable additional security features for production environments.
*/
var enableSecurityFeatures: Boolean = true
/**
* Connection pool size for better resource management.
*/
var connectionPoolSize: Int = 10
set(value) {
require(value > 0) { "Connection pool size must be positive" }
field = value
}
/**
* Optimized producer properties with performance tuning and reliability settings.
*/
fun producerConfigs(): Map<String, Any> = mapOf(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
// Avoid adding type info headers; keeps payloads simple and interoperable.
JsonSerializer.ADD_TYPE_INFO_HEADERS to false,
// Performance optimizations
ProducerConfig.BATCH_SIZE_CONFIG to 32768, // 32KB batch size for better throughput
ProducerConfig.LINGER_MS_CONFIG to 5, // Wait up to 5ms to batch messages
ProducerConfig.COMPRESSION_TYPE_CONFIG to "snappy", // Fast compression
ProducerConfig.BUFFER_MEMORY_CONFIG to 67108864, // 64MB buffer memory
// Reliability settings
ProducerConfig.ACKS_CONFIG to "all", // Wait for all replicas
ProducerConfig.RETRIES_CONFIG to 3, // Retry failed sends
ProducerConfig.RETRY_BACKOFF_MS_CONFIG to 1000, // 1 second retry backoff
ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG to 30000, // 30 second delivery timeout
ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG to 10000, // 10 second request timeout
// Idempotence for exactly-once semantics
ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG to true,
ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION to 5
)
/**
* Optimized consumer properties with performance tuning and reliability settings.
*/
fun consumerConfigs(groupId: String? = null): Map<String, Any> = mapOf(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers,
ConsumerConfig.GROUP_ID_CONFIG to (groupId ?: "${defaultGroupIdPrefix}-${System.currentTimeMillis()}"),
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java,
// JSON deserialization security
JsonDeserializer.TRUSTED_PACKAGES to trustedPackages,
JsonDeserializer.USE_TYPE_INFO_HEADERS to false,
// Performance optimizations
ConsumerConfig.FETCH_MIN_BYTES_CONFIG to 1024, // 1KB minimum fetch size
ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG to 500, // Max 500ms wait for fetch
ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG to 1048576, // 1MB max partition fetch
ConsumerConfig.MAX_POLL_RECORDS_CONFIG to 500, // Process up to 500 records per poll
// Reliability settings
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest",
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG to false, // Manual commit for better control
ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000, // 30 second session timeout
ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 3000, // 3 second heartbeat
// Connection settings
ConsumerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG to 540000, // 9 minutes idle timeout
ConsumerConfig.RECONNECT_BACKOFF_MS_CONFIG to 50,
ConsumerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG to 1000
)
/**
* Strongly typed producer factory to avoid unchecked casts in consumers/tests.
*/
fun producerFactory(): DefaultKafkaProducerFactory<String, Any> =
DefaultKafkaProducerFactory(producerConfigs())
}
@@ -0,0 +1,147 @@
package at.mocode.infrastructure.messaging.config
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class KafkaConfigTest {
@Test
fun `should validate bootstrap servers format`() {
val config = KafkaConfig()
// Valid formats
assertDoesNotThrow { config.bootstrapServers = "localhost:9092" }
assertDoesNotThrow { config.bootstrapServers = "PLAINTEXT://localhost:9092" }
assertDoesNotThrow { config.bootstrapServers = "host1:9092,host2:9092" }
assertDoesNotThrow { config.bootstrapServers = "PLAINTEXT://host1:9092,PLAINTEXT://host2:9092" }
assertDoesNotThrow { config.bootstrapServers = "kafka.example.com:9092" }
assertDoesNotThrow { config.bootstrapServers = "kafka-cluster-01.internal:9092" }
// Invalid formats
assertThrows<IllegalArgumentException> { config.bootstrapServers = "" }
assertThrows<IllegalArgumentException> { config.bootstrapServers = " " }
assertThrows<IllegalArgumentException> { config.bootstrapServers = "invalid-format" }
assertThrows<IllegalArgumentException> { config.bootstrapServers = "localhost" }
assertThrows<IllegalArgumentException> { config.bootstrapServers = ":9092" }
assertThrows<IllegalArgumentException> { config.bootstrapServers = "localhost:" }
assertThrows<IllegalArgumentException> { config.bootstrapServers = "localhost:abc" }
}
@Test
fun `should validate group ID prefix`() {
val config = KafkaConfig()
// Valid prefixes
assertDoesNotThrow { config.defaultGroupIdPrefix = "valid-prefix_123" }
assertDoesNotThrow { config.defaultGroupIdPrefix = "messaging-client" }
assertDoesNotThrow { config.defaultGroupIdPrefix = "test.group.id" }
assertDoesNotThrow { config.defaultGroupIdPrefix = "simple123" }
// Invalid prefixes
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = "" }
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = " " }
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = "invalid@prefix" }
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = "invalid#prefix" }
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = "invalid prefix" }
assertThrows<IllegalArgumentException> { config.defaultGroupIdPrefix = "invalid/prefix" }
}
@Test
fun `should validate trusted packages`() {
val config = KafkaConfig()
// Valid trusted packages
assertDoesNotThrow { config.trustedPackages = "at.mocode.*,com.example.*" }
assertDoesNotThrow { config.trustedPackages = "at.mocode.*" }
assertDoesNotThrow { config.trustedPackages = "com.example.specific.Package" }
assertDoesNotThrow { config.trustedPackages = "java.lang.*,java.util.*" }
// Invalid trusted packages
assertThrows<IllegalArgumentException> { config.trustedPackages = "" }
assertThrows<IllegalArgumentException> { config.trustedPackages = " " }
}
@Test
fun `should validate connection pool size`() {
val config = KafkaConfig()
// Valid pool sizes
assertDoesNotThrow { config.connectionPoolSize = 1 }
assertDoesNotThrow { config.connectionPoolSize = 5 }
assertDoesNotThrow { config.connectionPoolSize = 10 }
assertDoesNotThrow { config.connectionPoolSize = 100 }
// Invalid pool sizes
assertThrows<IllegalArgumentException> { config.connectionPoolSize = 0 }
assertThrows<IllegalArgumentException> { config.connectionPoolSize = -1 }
assertThrows<IllegalArgumentException> { config.connectionPoolSize = -10 }
}
@Test
fun `should have default values set correctly`() {
val config = KafkaConfig()
assertThat(config.bootstrapServers).isEqualTo("localhost:9092")
assertThat(config.defaultGroupIdPrefix).isEqualTo("messaging-client")
assertThat(config.trustedPackages).isEqualTo("at.mocode.*")
assertThat(config.enableSecurityFeatures).isEqualTo(true)
assertThat(config.connectionPoolSize).isEqualTo(10)
}
@Test
fun `should generate valid producer configs`() {
val config = KafkaConfig()
val producerConfigs = config.producerConfigs()
// Verify essential producer configuration
assertThat(producerConfigs["bootstrap.servers"]).isEqualTo("localhost:9092")
assertThat(producerConfigs["key.serializer"]).isEqualTo(org.apache.kafka.common.serialization.StringSerializer::class.java)
assertThat(producerConfigs["value.serializer"]).isEqualTo(org.springframework.kafka.support.serializer.JsonSerializer::class.java)
assertThat(producerConfigs["acks"]).isEqualTo("all")
assertThat(producerConfigs["enable.idempotence"]).isEqualTo(true)
}
@Test
fun `should generate valid consumer configs with custom group ID`() {
val config = KafkaConfig()
val customGroupId = "test-group-123"
val consumerConfigs = config.consumerConfigs(customGroupId)
// Verify essential consumer configuration
assertThat(consumerConfigs["bootstrap.servers"]).isEqualTo("localhost:9092")
assertThat(consumerConfigs["group.id"]).isEqualTo(customGroupId)
assertThat(consumerConfigs["key.deserializer"]).isEqualTo(org.apache.kafka.common.serialization.StringDeserializer::class.java)
assertThat(consumerConfigs["value.deserializer"]).isEqualTo(org.springframework.kafka.support.serializer.JsonDeserializer::class.java)
assertThat(consumerConfigs["spring.json.trusted.packages"]).isEqualTo("at.mocode.*")
assertThat(consumerConfigs["auto.offset.reset"]).isEqualTo("earliest")
assertThat(consumerConfigs["enable.auto.commit"]).isEqualTo(false)
}
@Test
fun `should generate unique consumer configs when no group ID provided`() {
val config = KafkaConfig()
val consumerConfigs1 = config.consumerConfigs()
val consumerConfigs2 = config.consumerConfigs()
// Group IDs should be different (timestamp-based)
val groupId1 = consumerConfigs1["group.id"].toString()
val groupId2 = consumerConfigs2["group.id"].toString()
assertThat(groupId1).isNotEqualTo(groupId2)
assertThat(groupId1).startsWith("messaging-client-")
assertThat(groupId2).startsWith("messaging-client-")
}
@Test
fun `should create producer factory with correct configuration`() {
val config = KafkaConfig()
val producerFactory = config.producerFactory()
assertDoesNotThrow { producerFactory.createProducer() }
assertThat(producerFactory.configurationProperties["bootstrap.servers"]).isEqualTo("localhost:9092")
}
}