(vision) SCS/DDD

This commit is contained in:
2025-07-18 23:07:05 +02:00
parent 029b0c86bc
commit 611e31e196
68 changed files with 6949 additions and 137 deletions
+2
View File
@@ -39,6 +39,8 @@ kotlin {
implementation(libs.ktor.server.callLogging)
implementation(libs.ktor.server.statusPages)
implementation(libs.ktor.server.serializationKotlinxJson)
implementation(libs.ktor.server.openapi)
implementation(libs.ktor.server.swagger)
implementation(libs.logback)
}
@@ -4,6 +4,8 @@ import at.mocode.gateway.config.configureDatabase
import at.mocode.gateway.config.configureSerialization
import at.mocode.gateway.config.configureMonitoring
import at.mocode.gateway.config.configureSecurity
import at.mocode.gateway.config.configureOpenApi
import at.mocode.gateway.config.configureSwagger
import at.mocode.gateway.routing.configureRouting
import io.ktor.server.application.*
import io.ktor.server.engine.*
@@ -37,6 +39,10 @@ fun Application.module() {
configureMonitoring()
configureSecurity()
// Configure API documentation
configureOpenApi()
configureSwagger()
// Configure routing - aggregates all bounded context routes
configureRouting()
}
@@ -0,0 +1,330 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.http.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import at.mocode.enums.RolleE
import at.mocode.enums.BerechtigungE
/**
* Authorization configuration and middleware for role-based access control.
*
* Provides utilities for checking user roles and permissions on protected endpoints.
*/
/**
* Enum representing user roles in the system.
*/
enum class UserRole {
ADMIN,
VEREINS_ADMIN,
FUNKTIONAER,
REITER,
TRAINER,
RICHTER,
TIERARZT,
ZUSCHAUER,
GAST
}
/**
* Enum representing permissions in the system.
*/
enum class Permission {
// Person management
PERSON_READ,
PERSON_CREATE,
PERSON_UPDATE,
PERSON_DELETE,
// Club management
VEREIN_READ,
VEREIN_CREATE,
VEREIN_UPDATE,
VEREIN_DELETE,
// Event management
VERANSTALTUNG_READ,
VERANSTALTUNG_CREATE,
VERANSTALTUNG_UPDATE,
VERANSTALTUNG_DELETE,
// Horse management
PFERD_READ,
PFERD_CREATE,
PFERD_UPDATE,
PFERD_DELETE,
// Master data management
STAMMDATEN_READ,
STAMMDATEN_UPDATE,
// System administration
SYSTEM_ADMIN,
BENUTZER_VERWALTEN,
ROLLEN_VERWALTEN
}
/**
* Data class representing user authorization context.
*/
data class UserAuthContext(
val userId: String,
val username: String,
val roles: List<UserRole>,
val permissions: List<Permission>
)
/**
* Maps domain role enum to authorization role enum.
*/
private fun mapDomainRoleToUserRole(domainRole: RolleE): UserRole {
return when (domainRole) {
RolleE.ADMIN -> UserRole.ADMIN
RolleE.VEREINS_ADMIN -> UserRole.VEREINS_ADMIN
RolleE.FUNKTIONAER -> UserRole.FUNKTIONAER
RolleE.REITER -> UserRole.REITER
RolleE.TRAINER -> UserRole.TRAINER
RolleE.RICHTER -> UserRole.RICHTER
RolleE.TIERARZT -> UserRole.TIERARZT
RolleE.ZUSCHAUER -> UserRole.ZUSCHAUER
RolleE.GAST -> UserRole.GAST
}
}
/**
* Maps domain permission enum to authorization permission enum.
*/
private fun mapDomainPermissionToPermission(domainPermission: BerechtigungE): Permission {
return when (domainPermission) {
BerechtigungE.PERSON_READ -> Permission.PERSON_READ
BerechtigungE.PERSON_CREATE -> Permission.PERSON_CREATE
BerechtigungE.PERSON_UPDATE -> Permission.PERSON_UPDATE
BerechtigungE.PERSON_DELETE -> Permission.PERSON_DELETE
BerechtigungE.VEREIN_READ -> Permission.VEREIN_READ
BerechtigungE.VEREIN_CREATE -> Permission.VEREIN_CREATE
BerechtigungE.VEREIN_UPDATE -> Permission.VEREIN_UPDATE
BerechtigungE.VEREIN_DELETE -> Permission.VEREIN_DELETE
BerechtigungE.VERANSTALTUNG_READ -> Permission.VERANSTALTUNG_READ
BerechtigungE.VERANSTALTUNG_CREATE -> Permission.VERANSTALTUNG_CREATE
BerechtigungE.VERANSTALTUNG_UPDATE -> Permission.VERANSTALTUNG_UPDATE
BerechtigungE.VERANSTALTUNG_DELETE -> Permission.VERANSTALTUNG_DELETE
BerechtigungE.PFERD_READ -> Permission.PFERD_READ
BerechtigungE.PFERD_CREATE -> Permission.PFERD_CREATE
BerechtigungE.PFERD_UPDATE -> Permission.PFERD_UPDATE
BerechtigungE.PFERD_DELETE -> Permission.PFERD_DELETE
BerechtigungE.STAMMDATEN_READ -> Permission.STAMMDATEN_READ
BerechtigungE.STAMMDATEN_UPDATE -> Permission.STAMMDATEN_UPDATE
BerechtigungE.SYSTEM_ADMIN -> Permission.SYSTEM_ADMIN
BerechtigungE.BENUTZER_VERWALTEN -> Permission.BENUTZER_VERWALTEN
BerechtigungE.ROLLEN_VERWALTEN -> Permission.ROLLEN_VERWALTEN
}
}
/**
* Extension function to get user authorization context from JWT principal.
*/
fun JWTPrincipal.getUserAuthContext(): UserAuthContext? {
val userId = getClaim("userId", String::class) ?: return null
val username = getClaim("username", String::class) ?: return null
// Get roles and permissions from JWT token
val domainRoles = getClaim("roles", Array<RolleE>::class)?.toList() ?: emptyList()
val domainPermissions = getClaim("permissions", Array<BerechtigungE>::class)?.toList() ?: emptyList()
// Map domain enums to authorization enums
val roles = domainRoles.map { mapDomainRoleToUserRole(it) }
val permissions = domainPermissions.map { mapDomainPermissionToPermission(it) }
return UserAuthContext(
userId = userId,
username = username,
roles = roles,
permissions = permissions
)
}
/**
* Maps roles to their corresponding permissions.
*/
private fun getRolePermissions(roles: List<UserRole>): List<Permission> {
val permissions = mutableSetOf<Permission>()
roles.forEach { role ->
when (role) {
UserRole.ADMIN -> {
permissions.addAll(Permission.values())
}
UserRole.VEREINS_ADMIN -> {
permissions.addAll(listOf(
Permission.PERSON_READ, Permission.PERSON_CREATE, Permission.PERSON_UPDATE,
Permission.VEREIN_READ, Permission.VEREIN_UPDATE,
Permission.PFERD_READ, Permission.PFERD_CREATE, Permission.PFERD_UPDATE,
Permission.STAMMDATEN_READ
))
}
UserRole.FUNKTIONAER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ, Permission.VERANSTALTUNG_CREATE, Permission.VERANSTALTUNG_UPDATE,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.TRAINER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.REITER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.RICHTER -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.VEREIN_READ,
Permission.VERANSTALTUNG_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.TIERARZT -> {
permissions.addAll(listOf(
Permission.PERSON_READ,
Permission.PFERD_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.ZUSCHAUER -> {
permissions.addAll(listOf(
Permission.VERANSTALTUNG_READ,
Permission.STAMMDATEN_READ
))
}
UserRole.GAST -> {
permissions.addAll(listOf(
Permission.STAMMDATEN_READ
))
}
}
}
return permissions.toList()
}
/**
* Route extension function to require specific roles.
*/
fun Route.requireRoles(vararg roles: UserRole, build: Route.() -> Unit): Route {
val route = createChild(object : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Constant
}
override fun toString(): String = "requireRoles(${roles.joinToString()})"
})
route.intercept(ApplicationCallPipeline.Call) {
val principal = call.principal<JWTPrincipal>()
val authContext = principal?.getUserAuthContext()
if (authContext == null) {
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
finish()
return@intercept
}
val hasRequiredRole = roles.any { requiredRole ->
authContext.roles.contains(requiredRole)
}
if (!hasRequiredRole) {
call.respond(
HttpStatusCode.Forbidden,
"Access denied. Required roles: ${roles.joinToString()}"
)
finish()
return@intercept
}
}
route.build()
return route
}
/**
* Route extension function to require specific permissions.
*/
fun Route.requirePermissions(vararg permissions: Permission, build: Route.() -> Unit): Route {
val route = createChild(object : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Constant
}
override fun toString(): String = "requirePermissions(${permissions.joinToString()})"
})
route.intercept(ApplicationCallPipeline.Call) {
val principal = call.principal<JWTPrincipal>()
val authContext = principal?.getUserAuthContext()
if (authContext == null) {
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
finish()
return@intercept
}
val hasAllPermissions = permissions.all { requiredPermission ->
authContext.permissions.contains(requiredPermission)
}
if (!hasAllPermissions) {
call.respond(
HttpStatusCode.Forbidden,
"Access denied. Required permissions: ${permissions.joinToString()}"
)
finish()
return@intercept
}
}
route.build()
return route
}
/**
* Pipeline context extension to get current user authorization context.
*/
val PipelineContext<Unit, ApplicationCall>.userAuthContext: UserAuthContext?
get() = call.principal<JWTPrincipal>()?.getUserAuthContext()
/**
* Application call extension to check if user has specific role.
*/
fun ApplicationCall.hasRole(role: UserRole): Boolean {
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
return authContext?.roles?.contains(role) == true
}
/**
* Application call extension to check if user has specific permission.
*/
fun ApplicationCall.hasPermission(permission: Permission): Boolean {
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
return authContext?.permissions?.contains(permission) == true
}
@@ -4,6 +4,7 @@ import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
/**
* Database configuration for the API Gateway.
@@ -11,6 +12,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
* Sets up database connections and schema initialization for all bounded contexts.
*/
fun Application.configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseConfig")
val databaseUrl = environment.config.propertyOrNull("database.url")?.getString()
?: "jdbc:postgresql://localhost:5432/meldestelle"
val databaseUser = environment.config.propertyOrNull("database.user")?.getString()
@@ -46,6 +48,11 @@ fun Application.configureDatabase() {
at.mocode.horses.infrastructure.repository.HorseTable
)
// Event Management Context tables
SchemaUtils.createMissingTablesAndColumns(
at.mocode.events.infrastructure.repository.VeranstaltungTable
)
log.info("Database schemas initialized successfully")
} catch (e: Exception) {
log.error("Failed to initialize database schemas: ${e.message}")
@@ -0,0 +1,50 @@
package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.plugins.openapi.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.routing.*
/**
* Configuration for OpenAPI/Swagger documentation.
*
* This module configures the OpenAPI specification generation and Swagger UI
* for the API Gateway, providing comprehensive API documentation.
*/
fun Application.configureOpenApi() {
install(OpenAPI) {
codegen = org.openapitools.codegen.CodegenType.CLIENT
info {
title = "Meldestelle Self-Contained Systems API"
version = "1.0.0"
description = "Unified API Gateway for Austrian Equestrian Federation bounded contexts"
contact {
name = "API Support"
email = "support@mocode.at"
}
license {
name = "MIT"
url = "https://opensource.org/licenses/MIT"
}
}
server("http://localhost:8080") {
description = "Development server"
}
server("https://api.meldestelle.at") {
description = "Production server"
}
}
}
/**
* Configuration for Swagger UI.
*
* Provides an interactive web interface for exploring and testing the API.
*/
fun Application.configureSwagger() {
routing {
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") {
version = "4.15.5"
}
}
}
@@ -2,12 +2,16 @@ package at.mocode.gateway.config
import io.ktor.server.application.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.http.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
/**
* Security configuration for the API Gateway.
*
* Configures CORS, authentication, and other security-related settings.
* Configures CORS, JWT authentication, and other security-related settings.
*/
fun Application.configureSecurity() {
install(CORS) {
@@ -29,10 +33,52 @@ fun Application.configureSecurity() {
anyHost() // This should be restricted in production
}
// TODO: Add JWT authentication configuration
// install(Authentication) {
// jwt("auth-jwt") {
// // JWT configuration
// }
// }
// JWT Configuration
val jwtConfig = JwtConfig.fromEnvironment()
install(Authentication) {
jwt("auth-jwt") {
realm = jwtConfig.realm
verifier(
JWT
.require(Algorithm.HMAC256(jwtConfig.secret))
.withAudience(jwtConfig.audience)
.withIssuer(jwtConfig.issuer)
.build()
)
validate { credential ->
if (credential.payload.getClaim("userId").asString() != null) {
JWTPrincipal(credential.payload)
} else {
null
}
}
challenge { defaultScheme, realm ->
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
}
}
}
}
/**
* JWT Configuration data class.
*/
data class JwtConfig(
val secret: String,
val issuer: String,
val audience: String,
val realm: String,
val expirationTime: Long = 3600000L // 1 hour in milliseconds
) {
companion object {
fun fromEnvironment(): JwtConfig {
return JwtConfig(
secret = System.getenv("JWT_SECRET") ?: "default-secret-key-change-in-production",
issuer = System.getenv("JWT_ISSUER") ?: "meldestelle-api",
audience = System.getenv("JWT_AUDIENCE") ?: "meldestelle-users",
realm = System.getenv("JWT_REALM") ?: "Meldestelle API",
expirationTime = System.getenv("JWT_EXPIRATION")?.toLongOrNull() ?: 3600000L
)
}
}
}
@@ -0,0 +1,318 @@
package at.mocode.gateway.routing
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
/**
* Authentication routes for the API Gateway.
*
* Provides endpoints for user login, logout, registration, and profile management.
* This is a simplified implementation that will be connected to the actual
* authentication services once the database layer is implemented.
*/
/**
* Data classes for API requests and responses
*/
@Serializable
data class LoginRequest(
val usernameOrEmail: String,
val password: String
)
@Serializable
data class LoginResponse(
val success: Boolean,
val token: String? = null,
val message: String? = null,
val user: UserProfileResponse? = null
)
@Serializable
data class RegisterRequest(
val personId: String, // UUID as string
val username: String,
val email: String,
val password: String
)
@Serializable
data class RegisterResponse(
val success: Boolean,
val message: String? = null,
val user: UserProfileResponse? = null,
val errors: List<ValidationErrorResponse>? = null
)
@Serializable
data class ValidationErrorResponse(
val field: String,
val message: String
)
@Serializable
data class UserProfileResponse(
val userId: String,
val username: String,
val email: String,
val isActive: Boolean,
val isEmailVerified: Boolean,
val lastLogin: String? = null
)
@Serializable
data class ChangePasswordRequest(
val currentPassword: String,
val newPassword: String
)
@Serializable
data class ChangePasswordResponse(
val success: Boolean,
val message: String? = null,
val errors: List<ValidationErrorResponse>? = null
)
/**
* Configures authentication routes
*/
fun Route.authRoutes(
authenticationService: at.mocode.members.domain.service.AuthenticationService,
jwtService: at.mocode.members.domain.service.JwtService
) {
route("/auth") {
// Login endpoint
post("/login") {
try {
val loginRequest = call.receive<LoginRequest>()
// Validate input
if (loginRequest.usernameOrEmail.isEmpty() || loginRequest.password.isEmpty()) {
call.respond(
HttpStatusCode.BadRequest,
LoginResponse(
success = false,
message = "Username/email and password are required"
)
)
return@post
}
// Authenticate user
val authResult = authenticationService.authenticate(
loginRequest.usernameOrEmail,
loginRequest.password
)
if (authResult.isSuccess) {
val user = authResult.user!!
val tokenInfo = authResult.tokenInfo!!
call.respond(
HttpStatusCode.OK,
LoginResponse(
success = true,
token = tokenInfo.token,
message = "Login successful",
user = UserProfileResponse(
userId = user.userId.toString(),
username = user.username,
email = user.email,
isActive = user.istAktiv,
isEmailVerified = user.istEmailVerifiziert,
lastLogin = user.letzteAnmeldung?.toString()
)
)
)
} else {
call.respond(
HttpStatusCode.Unauthorized,
LoginResponse(
success = false,
message = authResult.errorMessage ?: "Invalid credentials"
)
)
}
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
LoginResponse(
success = false,
message = "Invalid request: ${e.message}"
)
)
}
}
// Register endpoint
post("/register") {
try {
val registerRequest = call.receive<RegisterRequest>()
// TODO: Implement actual registration logic
// For now, return a mock response
if (registerRequest.username.isNotEmpty() &&
registerRequest.email.isNotEmpty() &&
registerRequest.password.length >= 8) {
call.respond(
HttpStatusCode.Created,
RegisterResponse(
success = true,
message = "User registered successfully",
user = UserProfileResponse(
userId = "mock-user-id-${System.currentTimeMillis()}",
username = registerRequest.username,
email = registerRequest.email,
isActive = true,
isEmailVerified = false,
lastLogin = null
)
)
)
} else {
val errors = mutableListOf<ValidationErrorResponse>()
if (registerRequest.username.isEmpty()) {
errors.add(ValidationErrorResponse("username", "Username is required"))
}
if (registerRequest.email.isEmpty()) {
errors.add(ValidationErrorResponse("email", "Email is required"))
}
if (registerRequest.password.length < 8) {
errors.add(ValidationErrorResponse("password", "Password must be at least 8 characters"))
}
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
success = false,
message = "Registration failed",
errors = errors
)
)
}
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
success = false,
message = "Invalid request: ${e.message}"
)
)
}
}
// Protected routes (require authentication)
authenticate("auth-jwt") {
// Get user profile
get("/profile") {
try {
val principal = call.principal<JWTPrincipal>()
val userId = principal?.getClaim("userId", String::class)
if (userId != null) {
// TODO: Fetch actual user data from database
call.respond(
HttpStatusCode.OK,
UserProfileResponse(
userId = userId,
username = "mock_user",
email = "mock@example.com",
isActive = true,
isEmailVerified = true,
lastLogin = null
)
)
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, "Error retrieving profile: ${e.message}")
}
}
// Change password
post("/change-password") {
try {
val principal = call.principal<JWTPrincipal>()
val userId = principal?.getClaim("userId", String::class)
if (userId != null) {
val changePasswordRequest = call.receive<ChangePasswordRequest>()
// TODO: Implement actual password change logic
if (changePasswordRequest.newPassword.length >= 8) {
call.respond(
HttpStatusCode.OK,
ChangePasswordResponse(
success = true,
message = "Password changed successfully"
)
)
} else {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = "Password change failed",
errors = listOf(
ValidationErrorResponse("newPassword", "Password must be at least 8 characters")
)
)
)
}
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
}
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = "Invalid request: ${e.message}"
)
)
}
}
// Refresh token
post("/refresh") {
try {
val token = call.request.header("Authorization")?.removePrefix("Bearer ")
if (token != null) {
// TODO: Implement actual token refresh logic
call.respond(
HttpStatusCode.OK,
mapOf(
"token" to "refreshed_mock_jwt_token_${System.currentTimeMillis()}",
"message" to "Token refreshed successfully"
)
)
} else {
call.respond(HttpStatusCode.BadRequest, "No token provided")
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, "Error refreshing token: ${e.message}")
}
}
// Logout (client-side token invalidation)
post("/logout") {
// In a stateless JWT system, logout is typically handled client-side
// by removing the token. For server-side logout, you would need a token blacklist.
call.respond(
HttpStatusCode.OK,
mapOf("message" to "Logged out successfully. Please remove the token from client storage.")
)
}
}
}
}
@@ -7,6 +7,11 @@ import at.mocode.masterdata.application.usecase.CreateCountryUseCase
import at.mocode.masterdata.application.usecase.GetCountryUseCase
import at.mocode.masterdata.infrastructure.api.CountryController
import at.mocode.masterdata.infrastructure.repository.LandRepositoryImpl
import at.mocode.members.domain.service.AuthenticationService
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.UserAuthorizationService
import at.mocode.members.domain.service.PasswordService
import at.mocode.members.infrastructure.repository.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
@@ -25,6 +30,29 @@ fun Application.configureRouting() {
val landRepository = LandRepositoryImpl()
val horseRepository = HorseRepositoryImpl()
// Initialize authentication repositories
val userRepository = UserRepositoryImpl()
val personRolleRepository = PersonRolleRepositoryImpl()
val rolleRepository = RolleRepositoryImpl()
val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl()
val berechtigungRepository = BerechtigungRepositoryImpl()
// Initialize authentication services
val passwordService = PasswordService()
val userAuthorizationService = UserAuthorizationService(
userRepository,
personRolleRepository,
rolleRepository,
rolleBerechtigungRepository,
berechtigungRepository
)
val jwtService = JwtService(userAuthorizationService)
val authenticationService = AuthenticationService(
userRepository,
passwordService,
jwtService
)
// Initialize use cases
val getCountryUseCase = GetCountryUseCase(landRepository)
val createCountryUseCase = CreateCountryUseCase(landRepository)
@@ -43,10 +71,12 @@ fun Application.configureRouting() {
version = "1.0.0",
description = "Self-Contained Systems API Gateway for Austrian Equestrian Federation",
availableContexts = listOf(
"authentication",
"master-data",
"horse-registry"
),
endpoints = mapOf(
"authentication" to "/auth/*",
"master-data" to "/api/masterdata/*",
"horse-registry" to "/api/horses/*"
)
@@ -60,6 +90,7 @@ fun Application.configureRouting() {
HealthStatus(
status = "UP",
contexts = mapOf(
"authentication" to "UP",
"master-data" to "UP",
"horse-registry" to "UP"
)
@@ -74,6 +105,11 @@ fun Application.configureRouting() {
title = "Meldestelle Self-Contained Systems API",
description = "Unified API Gateway for all bounded contexts",
contexts = listOf(
ContextInfo(
name = "Authentication Context",
path = "/auth",
description = "User authentication, registration, and profile management"
),
ContextInfo(
name = "Master Data Context",
path = "/api/masterdata",
@@ -91,6 +127,9 @@ fun Application.configureRouting() {
// Configure routes for each bounded context
// Authentication Routes
authRoutes(authenticationService, jwtService)
// Master Data Context Routes
countryController.configureRoutes(this)
@@ -0,0 +1,234 @@
package at.mocode.gateway
import at.mocode.dto.base.BaseDto
import at.mocode.gateway.routing.ApiGatewayInfo
import at.mocode.gateway.routing.HealthStatus
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
import kotlin.test.*
/**
* Integration tests for the API Gateway.
*
* These tests verify that all API endpoints are working correctly
* and that the OpenAPI/Swagger integration is functioning properly.
*/
class ApiIntegrationTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun testApiGatewayInfo() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("Meldestelle API Gateway"))
// Parse response as BaseDto
val response = json.decodeFromString<BaseDto<ApiGatewayInfo>>(responseText)
assertTrue(response.success)
assertNotNull(response.data)
assertEquals("Meldestelle API Gateway", response.data!!.name)
assertEquals("1.0.0", response.data!!.version)
assertTrue(response.data!!.availableContexts.contains("authentication"))
assertTrue(response.data!!.availableContexts.contains("master-data"))
assertTrue(response.data!!.availableContexts.contains("horse-registry"))
}
}
@Test
fun testHealthCheck() = testApplication {
application {
module()
}
client.get("/health").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
// Parse response as BaseDto
val response = json.decodeFromString<BaseDto<HealthStatus>>(responseText)
assertTrue(response.success)
assertNotNull(response.data)
assertEquals("UP", response.data!!.status)
assertTrue(response.data!!.contexts.containsKey("authentication"))
assertTrue(response.data!!.contexts.containsKey("master-data"))
assertTrue(response.data!!.contexts.containsKey("horse-registry"))
}
}
@Test
fun testApiDocumentation() = testApplication {
application {
module()
}
client.get("/api").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("Meldestelle Self-Contained Systems API"))
assertTrue(responseText.contains("Authentication Context"))
assertTrue(responseText.contains("Master Data Context"))
assertTrue(responseText.contains("Horse Registry Context"))
}
}
@Test
fun testSwaggerUI() = testApplication {
application {
module()
}
client.get("/swagger").apply {
// Swagger UI should be accessible (might return HTML or redirect)
assertTrue(status.isSuccess() || status == HttpStatusCode.Found)
}
}
@Test
fun testNotFoundEndpoint() = testApplication {
application {
module()
}
client.get("/nonexistent").apply {
assertEquals(HttpStatusCode.NotFound, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("Endpoint not found"))
}
}
@Test
fun testCorsHeaders() = testApplication {
application {
module()
}
client.options("/") {
header(HttpHeaders.Origin, "http://localhost:3000")
header(HttpHeaders.AccessControlRequestMethod, "GET")
}.apply {
// CORS should be configured
assertTrue(status.isSuccess())
}
}
@Test
fun testContentNegotiation() = testApplication {
application {
module()
}
client.get("/") {
header(HttpHeaders.Accept, "application/json")
}.apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(ContentType.Application.Json.withCharset(Charsets.UTF_8), contentType())
}
}
@Test
fun testMasterDataEndpoints() = testApplication {
application {
module()
}
// Test countries endpoint
client.get("/api/masterdata/countries").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("success"))
}
// Test active countries endpoint
client.get("/api/masterdata/countries/active").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
assertTrue(responseText.contains("success"))
}
}
@Test
fun testHorseRegistryEndpoints() = testApplication {
application {
module()
}
// Test horses endpoint (should require authentication)
client.get("/api/horses").apply {
// Should return unauthorized or redirect to login
assertTrue(status == HttpStatusCode.Unauthorized || status == HttpStatusCode.Found)
}
// Test horse stats endpoint
client.get("/api/horses/stats").apply {
// Should require authentication
assertTrue(status == HttpStatusCode.Unauthorized || status == HttpStatusCode.Found)
}
}
@Test
fun testAuthenticationEndpoints() = testApplication {
application {
module()
}
// Test registration endpoint structure
client.post("/auth/register") {
contentType(ContentType.Application.Json)
setBody("""
{
"email": "test@example.com",
"password": "TestPassword123!",
"firstName": "Test",
"lastName": "User",
"phoneNumber": "+43123456789"
}
""".trimIndent())
}.apply {
// Should process the request (might fail due to validation or database issues)
assertTrue(status.value in 200..499)
}
// Test login endpoint structure
client.post("/auth/login") {
contentType(ContentType.Application.Json)
setBody("""
{
"email": "test@example.com",
"password": "TestPassword123!"
}
""".trimIndent())
}.apply {
// Should process the request
assertTrue(status.value in 200..499)
}
}
@Test
fun testApiResponseFormat() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
val responseText = bodyAsText()
// Verify BaseDto structure
assertTrue(responseText.contains("\"success\""))
assertTrue(responseText.contains("\"data\""))
assertTrue(responseText.contains("\"message\""))
// Should be valid JSON
assertNotNull(json.decodeFromString<BaseDto<ApiGatewayInfo>>(responseText))
}
}
}