diff --git a/Dockerfile b/Dockerfile index 85afb8e..6f96752 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM gradle:7.4.2-jdk17 AS dependencies WORKDIR /home/gradle/usvision -RUN mkdir app-analyses app-model app-persistence app-reports app-web \ +RUN mkdir app-analyses app-model app-persistence app-reports app-web app-creation \ && chown gradle:gradle . -R USER gradle @@ -20,6 +20,7 @@ COPY --chown=gradle:gradle app-model/build.gradle.kts app-model/gradle COPY --chown=gradle:gradle app-persistence/build.gradle.kts app-persistence/gradle.properties ./app-persistence/ COPY --chown=gradle:gradle app-reports/build.gradle.kts app-reports/gradle.properties ./app-reports/ COPY --chown=gradle:gradle app-web/build.gradle.kts app-web/gradle.properties ./app-web/ +COPY --chown=gradle:gradle app-creation/build.gradle.kts app-creation/gradle.properties ./app-creation # PRE-INSTALL JUST THE DEPENDENCIES -- THIS SHALL SPEEDUP FUTURE BUILDS diff --git a/app-creation/build.gradle.kts b/app-creation/build.gradle.kts new file mode 100644 index 0000000..dc8c0e6 --- /dev/null +++ b/app-creation/build.gradle.kts @@ -0,0 +1,24 @@ +val mockk_version: String by project + +plugins { + `java-library` + kotlin("jvm") + kotlin("plugin.serialization") +} + +group = "com.usvision.reports" +version = "0.0.1" + +repositories { + mavenCentral() +} + +tasks.test { + useJUnitPlatform() +} + +dependencies { + implementation(project(":app-model")) + testImplementation("io.mockk:mockk:${mockk_version}") + testImplementation(kotlin("test")) +} diff --git a/app-creation/gradle.properties b/app-creation/gradle.properties new file mode 100644 index 0000000..c65e709 --- /dev/null +++ b/app-creation/gradle.properties @@ -0,0 +1,2 @@ +mockk_version=1.12.3 +kotlin.code.style=official diff --git a/app-creation/src/main/kotlin/com/usvision/creation/SystemAggregateStorage.kt b/app-creation/src/main/kotlin/com/usvision/creation/SystemAggregateStorage.kt new file mode 100644 index 0000000..8dfa35c --- /dev/null +++ b/app-creation/src/main/kotlin/com/usvision/creation/SystemAggregateStorage.kt @@ -0,0 +1,13 @@ +package com.usvision.creation + +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.Microservice +import com.usvision.model.systemcomposite.System + +interface SystemAggregateStorage { + fun getSystem(name: String): System? + fun save(microservice: Microservice): Microservice + fun save(companySystem: CompanySystem): CompanySystem + fun getCompanySystem(name: String): CompanySystem? + fun getMicroservice(name: String): Microservice? +} \ No newline at end of file diff --git a/app-creation/src/main/kotlin/com/usvision/creation/SystemCreator.kt b/app-creation/src/main/kotlin/com/usvision/creation/SystemCreator.kt new file mode 100644 index 0000000..91f32d9 --- /dev/null +++ b/app-creation/src/main/kotlin/com/usvision/creation/SystemCreator.kt @@ -0,0 +1,99 @@ +package com.usvision.creation + +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.Microservice +import com.usvision.model.domain.databases.Database +import com.usvision.model.domain.operations.Operation +import com.usvision.model.systemcomposite.System + +typealias CompanySystemDTO = CompanySystem +typealias SystemDTO = System +typealias MicroserviceDTO = Microservice +typealias DatabaseDTO = Database + +class SystemCreator( + private val systemAggregateStorage: SystemAggregateStorage +) { + + fun createCompanySystem(companySystem: CompanySystemDTO, fatherSystemName: String? = null): CompanySystemDTO { + checkIfSystemAlreadyExists(companySystem.name) + + fatherSystemName?.also { + val fatherSystem = systemAggregateStorage.getCompanySystem(fatherSystemName) + + return@createCompanySystem fatherSystem?.let { + fatherSystem.addSubsystem(companySystem) + systemAggregateStorage.save(fatherSystem) + } ?: throw Exception("A System Of Systems with name $fatherSystemName does not exist") + } + + return systemAggregateStorage.save(companySystem) + } + + fun createMicroservice(microservice: MicroserviceDTO, fatherSystemName: String? = null): SystemDTO { + checkIfSystemAlreadyExists(microservice.name) + + fatherSystemName?.also { + val fatherSystem = systemAggregateStorage.getCompanySystem(fatherSystemName) + + return@createMicroservice fatherSystem?.let { + fatherSystem.addSubsystem(microservice) + systemAggregateStorage.save(fatherSystem) + } ?: throw Exception("A System Of Systems with name $fatherSystemName does not exist") + } + + return systemAggregateStorage.save(microservice) + } + + fun addNewDatabaseConnectionToMicroservice( + database: DatabaseDTO, + microserviceName: String + ) = getExistingMicroservice(microserviceName).let { + it.addDatabaseConnection(database) + systemAggregateStorage.save(it) + } + + fun addOperationsToMicroservice( + exposedOperations: List, + consumedOperations: List, + microserviceName: String + ) = getExistingMicroservice(microserviceName).let { + exposedOperations.forEach { + operation -> it.exposeOperation(operation) + } + + consumedOperations.forEach { + operation -> it.consumeOperation(operation) + } + + systemAggregateStorage.save(it) + } + + fun addMessageChannelsToMicroservice( + publishMessageChannels: List, + subscribedMessageChannels: List, + microserviceName: String + ) = getExistingMicroservice(microserviceName).let { + publishMessageChannels.forEach { + operation -> it.addPublishChannel(operation) + } + + subscribedMessageChannels.forEach { + operation -> it.addSubscribedChannel(operation) + } + + systemAggregateStorage.save(it) + } + + private fun getExistingMicroservice( + microserviceName: String + ) = systemAggregateStorage.getMicroservice(microserviceName) + ?: throw Exception("A Microservice with name $microserviceName does not exist") + + private fun checkIfSystemAlreadyExists(name: String) { + systemAggregateStorage.getSystem(name)?.also { + throw Exception("A system with name $name already exists") + } + } +} \ No newline at end of file diff --git a/app-creation/src/test/kotlin/com/usvision/creation/SystemCreatorTest.kt b/app-creation/src/test/kotlin/com/usvision/creation/SystemCreatorTest.kt new file mode 100644 index 0000000..01cd21d --- /dev/null +++ b/app-creation/src/test/kotlin/com/usvision/creation/SystemCreatorTest.kt @@ -0,0 +1,441 @@ +package com.usvision.creation + +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.databases.PostgreSQL +import com.usvision.model.domain.operations.RestEndpoint +import com.usvision.model.systemcomposite.System +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import kotlin.test.* + +internal class SystemCreatorTest { + private val systemAggregateStorage = mockk() + private val systemCreator = SystemCreator(systemAggregateStorage) + + @Test + fun `it saves a CompanySystem and returns itself when a fatherName is not passed as argument`() { + val systemName = "name" + val companySystem = CompanySystemDTO(systemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.save(companySystem) } returns companySystem + + val resultCompanySystem = assertDoesNotThrow { + systemCreator.createCompanySystem(companySystem) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.save(companySystem) + } + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + + assertEquals(companySystem, resultCompanySystem) + } + + @Test + fun `it adds a CompanySystem to the father system and returns it when a CompanySystem and a fatherName are passed as arguments`() { + val systemName = "name" + val fatherSystemName = "fatherName" + val companySystem = CompanySystemDTO(systemName) + + val fatherCompanySystem = CompanySystemDTO(fatherSystemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.getCompanySystem(fatherSystemName) } returns fatherCompanySystem + every { systemAggregateStorage.save(fatherCompanySystem) } returns fatherCompanySystem + + val resultCompanySystem = assertDoesNotThrow { + systemCreator.createCompanySystem(companySystem, fatherSystemName) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.getCompanySystem(fatherSystemName) + systemAggregateStorage.save(fatherCompanySystem) + } + verify(exactly = 0) { + systemAggregateStorage.save(any()) + } + + assertEquals(fatherCompanySystem, resultCompanySystem) + assertContains(resultCompanySystem.getSubsystemSet(), companySystem) + } + + @Test + fun `it throws Exception when CompanySystem and a fatherName are passed as arguments and father System does not exist`() { + val systemName = "name" + val fatherSystemName = "fatherName" + val companySystem = CompanySystemDTO(systemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.getCompanySystem(fatherSystemName) } returns null + + assertThrows { + systemCreator.createCompanySystem(companySystem, fatherSystemName) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.getCompanySystem(fatherSystemName) + } + verify(exactly = 0) { + systemAggregateStorage.save(any()) + systemAggregateStorage.save(any()) + } + } + + @Test + fun `it throws exception when a system with the same name already exists when attempting to create a CompanySystem`() { + val systemName = "name" + + every { systemAggregateStorage.getSystem(systemName) } returns mockk() + + assertThrows { + systemCreator.createCompanySystem(CompanySystemDTO(systemName)) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + } + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + systemAggregateStorage.save(any()) + } + } + + + + @Test + fun `it saves a Microservice and returns itself when a fatherName is not passed as argument`() { + val systemName = "name" + val microservice = MicroserviceDTO(systemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.save(microservice) } returns microservice + + val resultCompanySystem = assertDoesNotThrow { + systemCreator.createMicroservice(microservice) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.save(microservice) + } + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + + assertEquals(microservice, resultCompanySystem) + } + + @Test + fun `it adds a Microservice to the father system and returns it when a Microservice and a fatherName are passed as arguments`() { + val systemName = "name" + val fatherSystemName = "fatherName" + val microservice = MicroserviceDTO(systemName) + + val fatherCompanySystem = CompanySystemDTO(fatherSystemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.getCompanySystem(fatherSystemName) } returns fatherCompanySystem + every { systemAggregateStorage.save(fatherCompanySystem) } returns fatherCompanySystem + + val resultCompanySystem = assertDoesNotThrow { + systemCreator.createMicroservice(microservice, fatherSystemName) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.getCompanySystem(fatherSystemName) + systemAggregateStorage.save(fatherCompanySystem) + } + verify(exactly = 0) { + systemAggregateStorage.save(any()) + } + + assertEquals(fatherCompanySystem, resultCompanySystem) + assertIs(resultCompanySystem) + assertContains(resultCompanySystem.getSubsystemSet(), microservice) + } + + @Test + fun `it throws Exception when Microservice and a fatherName are passed as arguments and father System does not exist`() { + val systemName = "name" + val fatherSystemName = "fatherName" + val microservice = MicroserviceDTO(systemName) + + every { systemAggregateStorage.getSystem(systemName) } returns null + every { systemAggregateStorage.getCompanySystem(fatherSystemName) } returns null + + assertThrows { + systemCreator.createMicroservice(microservice, fatherSystemName) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + systemAggregateStorage.getCompanySystem(fatherSystemName) + } + verify(exactly = 0) { + systemAggregateStorage.save(any()) + systemAggregateStorage.save(any()) + } + } + + @Test + fun `it throws exception when a system with the same name already exists when attempting to create a Microservice`() { + val systemName = "name" + + every { systemAggregateStorage.getSystem(systemName) } returns mockk() + + assertThrows { + systemCreator.createMicroservice(MicroserviceDTO(systemName)) + } + + verify(exactly = 1) { + systemAggregateStorage.getSystem(systemName) + } + + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + systemAggregateStorage.save(any()) + } + } + + @Test + fun `it adds rest endpoints to Microservice and saves it`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val consumedRestEndpoints = listOf( + RestEndpoint(httpVerb = "GET", path = "/test") + ) + + val exposedRestEndpoints = listOf( + RestEndpoint(httpVerb = "POST", path = "/test") + ) + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns microservice + every { systemAggregateStorage.save(microservice) } returns microservice + + val addRestEndpointsResult = assertDoesNotThrow { + systemCreator.addOperationsToMicroservice( + exposedOperations = exposedRestEndpoints, + consumedOperations = consumedRestEndpoints, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + systemAggregateStorage.save(microservice) + } + + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + + assertContains(addRestEndpointsResult.getExposedOperations(), exposedRestEndpoints.first()) + assertContains(addRestEndpointsResult.getConsumedOperations(), consumedRestEndpoints.first()) + + assertTrue { + addRestEndpointsResult.getExposedOperations().all { + operation -> operation != consumedRestEndpoints.first() + } + } + + assertTrue { + addRestEndpointsResult.getConsumedOperations().all { + operation -> operation != exposedRestEndpoints.first() + } + } + } + + @Test + fun `it throws exception when attempting to add rest endpoints to non-existent Microservice`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val consumedRestEndpoints = listOf( + RestEndpoint(httpVerb = "GET", path = "/test") + ) + + val exposedRestEndpoints = listOf( + RestEndpoint(httpVerb = "POST", path = "/test") + ) + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns null + + assertThrows { + systemCreator.addOperationsToMicroservice( + exposedOperations = exposedRestEndpoints, + consumedOperations = consumedRestEndpoints, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + } + + verify(exactly = 0) { + systemAggregateStorage.save(microservice) + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + } + + @Test + fun `it adds message channels to Microservice and saves it`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val publishMessageChannels = listOf( + MessageChannel(name="publish.test") + ) + + val subscribedMessageChannels = listOf( + MessageChannel(name="subscribe.test") + ) + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns microservice + every { systemAggregateStorage.save(microservice) } returns microservice + + val addRestEndpointsResult = assertDoesNotThrow { + systemCreator.addMessageChannelsToMicroservice( + publishMessageChannels = publishMessageChannels, + subscribedMessageChannels = subscribedMessageChannels, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + systemAggregateStorage.save(microservice) + } + + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + + assertContains(addRestEndpointsResult.getPublishChannels(), publishMessageChannels.first()) + assertContains(addRestEndpointsResult.getSubscribedChannels(), subscribedMessageChannels.first()) + + assertTrue { + addRestEndpointsResult.getPublishChannels().all { + operation -> operation != subscribedMessageChannels.first() + } + } + + assertTrue { + addRestEndpointsResult.getSubscribedChannels().all { + operation -> operation != publishMessageChannels.first() + } + } + } + + @Test + fun `it throws exception when attempting to add message channels to non-existent Microservice`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val publishMessageChannels = listOf( + MessageChannel(name="publish.test") + ) + + val subscribedMessageChannels = listOf( + MessageChannel(name="subscribe.test") + ) + + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns null + + + assertThrows { + systemCreator.addMessageChannelsToMicroservice( + publishMessageChannels = publishMessageChannels, + subscribedMessageChannels = subscribedMessageChannels, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + } + + verify(exactly = 0) { + systemAggregateStorage.save(microservice) + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + } + + @Test + fun `it adds a database to Microservice and saves it`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val database = PostgreSQL() + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns microservice + every { systemAggregateStorage.save(microservice) } returns microservice + + + val addRestEndpointsResult = assertDoesNotThrow { + systemCreator.addNewDatabaseConnectionToMicroservice( + database = database, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + systemAggregateStorage.save(microservice) + } + + verify(exactly = 0) { + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + + assertContains(addRestEndpointsResult.getDatabases(), database) + } + + @Test + fun `it throws exception when attempting to add a database to non-existent Microservice`() { + val microserviceName = "name" + val microservice = MicroserviceDTO(microserviceName) + + val database = PostgreSQL() + + every { systemAggregateStorage.getMicroservice(microserviceName) } returns null + + assertThrows { + systemCreator.addNewDatabaseConnectionToMicroservice( + database = database, + microserviceName = microserviceName + ) + } + + verify(exactly = 1) { + systemAggregateStorage.getMicroservice(microserviceName) + } + + verify(exactly = 0) { + systemAggregateStorage.save(microservice) + systemAggregateStorage.getCompanySystem(any()) + systemAggregateStorage.save(any()) + } + } +} \ No newline at end of file diff --git a/app-model/src/main/kotlin/com/usvision/model/domain/Module.kt b/app-model/src/main/kotlin/com/usvision/model/domain/Module.kt index 649f26b..cda8ffe 100644 --- a/app-model/src/main/kotlin/com/usvision/model/domain/Module.kt +++ b/app-model/src/main/kotlin/com/usvision/model/domain/Module.kt @@ -7,7 +7,7 @@ import java.util.UUID @Serializable data class Module( - val id: String? + val id: String ) : Visitable { companion object { fun createWithId() = Module(id = UUID.randomUUID().toString()) diff --git a/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownDatabaseClassException.kt b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownDatabaseClassException.kt new file mode 100644 index 0000000..02dee20 --- /dev/null +++ b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownDatabaseClassException.kt @@ -0,0 +1,4 @@ +package com.usvision.model.exceptions + +class UnknownDatabaseClassException(className: String) + : RuntimeException("Database has a unknown class to $className") diff --git a/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownOperationClassException.kt b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownOperationClassException.kt new file mode 100644 index 0000000..d4364f6 --- /dev/null +++ b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownOperationClassException.kt @@ -0,0 +1,4 @@ +package com.usvision.model.exceptions + +class UnknownOperationClassException(className: String) + : RuntimeException("Operation has a unknown class to $className") \ No newline at end of file diff --git a/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownSystemClassException.kt b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownSystemClassException.kt new file mode 100644 index 0000000..79e63fa --- /dev/null +++ b/app-model/src/main/kotlin/com/usvision/model/exceptions/UnknownSystemClassException.kt @@ -0,0 +1,4 @@ +package com.usvision.model.exceptions + +class UnknownSystemClassException(name: String, className: String) + : RuntimeException("System $name has a unknown class to $className") diff --git a/app-persistence/build.gradle.kts b/app-persistence/build.gradle.kts index 9ff33b0..85ad114 100644 --- a/app-persistence/build.gradle.kts +++ b/app-persistence/build.gradle.kts @@ -21,10 +21,12 @@ tasks.test { dependencies { implementation(project(":app-model")) implementation(project(":app-reports")) + implementation(project(":app-creation")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutine_version") implementation("org.mongodb:mongodb-driver-kotlin-coroutine:$mongodb_version") testImplementation(kotlin("test")) testImplementation("io.mockk:mockk:$mockk_version") + testImplementation("org.junit.jupiter:junit-jupiter-params") } \ No newline at end of file diff --git a/app-persistence/src/main/kotlin/com/usvision/persistence/documents/SystemDocument.kt b/app-persistence/src/main/kotlin/com/usvision/persistence/documents/SystemDocument.kt index 960c816..6e40c00 100644 --- a/app-persistence/src/main/kotlin/com/usvision/persistence/documents/SystemDocument.kt +++ b/app-persistence/src/main/kotlin/com/usvision/persistence/documents/SystemDocument.kt @@ -1,6 +1,15 @@ package com.usvision.persistence.documents +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.Microservice +import com.usvision.model.domain.Module +import com.usvision.model.domain.databases.Database +import com.usvision.model.domain.operations.Operation import com.usvision.model.domain.operations.RestEndpoint +import com.usvision.model.systemcomposite.System +import com.usvision.model.exceptions.UnknownOperationClassException +import com.usvision.model.exceptions.UnknownSystemClassException import org.bson.Document import org.bson.codecs.pojo.annotations.BsonId import org.bson.types.ObjectId @@ -17,14 +26,6 @@ data class SystemDocument( val module: ModuleDocument? = null ) -fun RestEndpoint.toDocument(): Document { - return Document().also { doc: Document -> - doc["description"] = this.description - doc["httpVerb"] = this.httpVerb - doc["path"] = this.path - } -} - data class DatabaseDocument( @BsonId val id: ObjectId, val description: String @@ -38,4 +39,69 @@ data class MessageChannelDocument( data class ModuleDocument( @BsonId val id: ObjectId, val uuid: String -) \ No newline at end of file +) + +fun CompanySystem.toSystemDocument( + id: ObjectId = ObjectId() +): SystemDocument = SystemDocument( + id = id, + name = this.name, + subsystems = this.getSubsystemSet().toSystemDocumentSet() +) + +fun Microservice.toSystemDocument( + id: ObjectId = ObjectId() +): SystemDocument = SystemDocument( + id = id, + name = this.name, + module = this.module.toModuleDocument(), + databases = this.getDatabases().toDatabaseDocumentSet(), + exposedOperations = this.getExposedOperations().toOperationDocumentSet(), + consumedOperations = this.getConsumedOperations().toOperationDocumentSet(), + publishedChannels = this.getPublishChannels().toMessageChannelDocumentSet(), + subscribedChannels = this.getSubscribedChannels().toMessageChannelDocumentSet(), +) + +private fun System.toSystemDocument( + id: ObjectId = ObjectId() +) = when (this) { + is CompanySystem -> { + this.toSystemDocument(id) + } + is Microservice -> { + this.toSystemDocument(id) + } + else -> { + throw UnknownSystemClassException(this.name, "SystemDocument") + } +} + +private fun Set.toSystemDocumentSet(): Set = this.map { it.toSystemDocument() }.toSet() + +private fun Set.toDatabaseDocumentSet(): Set = this.map { it.toDatabaseDocument() }.toSet() + +private fun Set.toOperationDocumentSet(): Set = this.map { it.toDocument() }.toSet() + +fun RestEndpoint.toDocument(): Document { + return Document().also { doc: Document -> + doc["description"] = this.description + doc["httpVerb"] = this.httpVerb + doc["path"] = this.path + } +} + +private fun Set.toMessageChannelDocumentSet(): Set = this.map { it.toMessageChannelDocument() }.toSet() + +private fun MessageChannel.toMessageChannelDocument(id: ObjectId = ObjectId()) = MessageChannelDocument(id = id, name) + +private fun Module.toModuleDocument() = ModuleDocument(id = ObjectId(), uuid = id) + +private fun Operation.toDocument() = when (this) { + is RestEndpoint -> this.toDocument() + else -> throw UnknownOperationClassException("SystemDocument") +} + +private fun Database.toDatabaseDocument() = DatabaseDocument( + id = this.id?.let { ObjectId(this.id) } ?: ObjectId(), + description = this.description +) diff --git a/app-persistence/src/main/kotlin/com/usvision/persistence/repositories/MongoSystemRepository.kt b/app-persistence/src/main/kotlin/com/usvision/persistence/repositories/MongoSystemRepository.kt index 53c05bb..2152137 100644 --- a/app-persistence/src/main/kotlin/com/usvision/persistence/repositories/MongoSystemRepository.kt +++ b/app-persistence/src/main/kotlin/com/usvision/persistence/repositories/MongoSystemRepository.kt @@ -3,16 +3,20 @@ package com.usvision.persistence.repositories import com.mongodb.client.model.Filters import com.mongodb.kotlin.client.coroutine.MongoCollection import com.mongodb.kotlin.client.coroutine.MongoDatabase +import com.usvision.creation.SystemAggregateStorage +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.Microservice import com.usvision.model.systemcomposite.System import com.usvision.persistence.documents.SystemDocument +import com.usvision.persistence.documents.toSystemDocument import com.usvision.persistence.exceptions.SystemNotFoundException import com.usvision.reports.SystemRepository -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.runBlocking -import java.util.NoSuchElementException +import org.bson.types.ObjectId -class MongoSystemRepository(db: MongoDatabase) : SystemRepository { + +class MongoSystemRepository(db: MongoDatabase) : SystemRepository, SystemAggregateStorage { companion object { const val COLLECTION_NAME = "systems" } @@ -23,15 +27,49 @@ class MongoSystemRepository(db: MongoDatabase) : SystemRepository { systemCollection = db.getCollection(COLLECTION_NAME) } - override fun load(name: String): System = try { - runBlocking { - systemCollection - .find(Filters.eq("name", name)) - .limit(1) - .map { SystemMapper.fromDocument(it) } - .first() - } - } catch (nsee: NoSuchElementException) { - throw SystemNotFoundException(name) + + override fun load(name: String): System = getSystem(name) ?: throw SystemNotFoundException(name) + + override fun getSystem(name: String): System? = runBlocking { + systemCollection + .find(Filters.eq("name", name)) + .firstOrNull() + ?.let { SystemMapper.fromDocument(it) } + } + + + override fun save(companySystem: CompanySystem): CompanySystem = runBlocking { + val insertedId = systemCollection.insertOne( + companySystem.toSystemDocument() + ).insertedId ?: throw SystemNotFoundException(companySystem.name) + + getSystemById(insertedId.asObjectId().value) as CompanySystem + } + + override fun save(microservice: Microservice): Microservice = runBlocking { + val insertedId = systemCollection.insertOne( + microservice.toSystemDocument() + ).insertedId ?: throw SystemNotFoundException(microservice.name) + + getSystemById(insertedId.asObjectId().value) as Microservice + } + + override fun getCompanySystem(name: String): CompanySystem? = try { + getSystem(name = name) as CompanySystem? + } catch (ex: ClassCastException) { + null + } + + override fun getMicroservice(name: String): Microservice? = try { + getSystem(name = name) as Microservice? + } catch (ex: ClassCastException) { + null + } + + private fun getSystemById(id: ObjectId): System? = runBlocking { + systemCollection + .find(Filters.eq("_id", id)) + .firstOrNull() + ?.let { SystemMapper.fromDocument(it) } } } diff --git a/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/DBRepositoryProvider.kt b/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/DBRepositoryProvider.kt index 16fb964..7b1d380 100644 --- a/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/DBRepositoryProvider.kt +++ b/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/DBRepositoryProvider.kt @@ -1,7 +1,9 @@ package com.usvision.persistence.repositorybuilder +import com.usvision.creation.SystemAggregateStorage import com.usvision.reports.SystemRepository interface DBRepositoryProvider : DBConnectionBuilder { fun getRepository(): SystemRepository + fun getAggregateStorage(): SystemAggregateStorage } diff --git a/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/MongoDBRepositoryProvider.kt b/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/MongoDBRepositoryProvider.kt index 462818c..3728aee 100644 --- a/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/MongoDBRepositoryProvider.kt +++ b/app-persistence/src/main/kotlin/com/usvision/persistence/repositorybuilder/MongoDBRepositoryProvider.kt @@ -2,6 +2,7 @@ package com.usvision.persistence.repositorybuilder import com.mongodb.kotlin.client.coroutine.MongoClient import com.mongodb.kotlin.client.coroutine.MongoDatabase +import com.usvision.creation.SystemAggregateStorage import com.usvision.persistence.repositories.MongoSystemRepository class MongoDBRepositoryProvider : DBRepositoryProvider, DBConnectionProvider { @@ -38,7 +39,11 @@ class MongoDBRepositoryProvider : DBRepositoryProvider, DBConnectionProvider underTest.save(system) + is CompanySystem -> underTest.save(system) + } + + assertEquals(system, underTest.getSystem(system.name)) + } + + @Test + fun `it returns null when no such name is found while trying to get a System`() { + assertNull(underTest.getSystem("systemName")) + } + + @Test + fun `it updates existing CompanySystem`() { + val companySystem = CompanySystem( + name = "companySystemName" + ) + + underTest.save(companySystem) + + val updatedCompanySystem = companySystem.copy().apply { + addSubsystem(Microservice(name = "microserviceName")) + } + + underTest.save(updatedCompanySystem) + + assertEquals(updatedCompanySystem, underTest.getCompanySystem("companySystemName")) + } + + @Test + fun `it updates existing Microservice`() { + val microservice = Microservice( + name = "microserviceName" + ) + + underTest.save(microservice) + + val updatedMicroservice = microservice.copy().apply { + addDatabaseConnection(PostgreSQL()) + addPublishChannel(MessageChannel("publishChannel")) + addSubscribedChannel(MessageChannel("subscribeChannel")) + consumeOperation(RestEndpoint("GET", "/test")) + exposeOperation(RestEndpoint("POST", "/test")) + } + + underTest.save(updatedMicroservice) + + assertEquals(updatedMicroservice, underTest.getMicroservice("microserviceName")) + } + + private fun createSystemWithoutSubsysNorModule(name: String) = runBlocking { systemsCollection.insertOne( SystemDocument( @@ -305,4 +464,12 @@ internal class MongoSystemRepositoryTest { ) ) } + + companion object { + @JvmStatic + fun systemProvider(): Stream = Stream.of( + Arguments.of(Microservice(name = "microserviceName")), + Arguments.of(CompanySystem(name = "companySystemName")) + ) + } } \ No newline at end of file diff --git a/app-web/build.gradle.kts b/app-web/build.gradle.kts index f8f3f11..b251fbc 100644 --- a/app-web/build.gradle.kts +++ b/app-web/build.gradle.kts @@ -1,8 +1,12 @@ +val mockk_version: String by project +val kotlinx_serialization_version: String by project + plugins { application kotlin("jvm") id("io.ktor.plugin") version "2.3.5" id("com.github.johnrengelman.shadow") version "7.0.0" + kotlin("plugin.serialization") } group = "com.usvision.web" @@ -26,6 +30,8 @@ repositories { dependencies { implementation(project(":app-reports")) + implementation(project(":app-creation")) + implementation(project(":app-model")) implementation(project(":app-persistence")) implementation("io.ktor:ktor-server-core") @@ -39,4 +45,10 @@ dependencies { implementation("io.ktor:ktor-server-host-common-jvm") implementation("io.ktor:ktor-server-status-pages-jvm") implementation("io.ktor:ktor-server-config-yaml") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinx_serialization_version}") + + testImplementation("io.mockk:mockk:${mockk_version}") + testImplementation("io.ktor:ktor-server-test-host") + testImplementation("io.ktor:ktor-client-content-negotiation") + testImplementation(kotlin("test")) } \ No newline at end of file diff --git a/app-web/gradle.properties b/app-web/gradle.properties index 29e08e8..be79e87 100644 --- a/app-web/gradle.properties +++ b/app-web/gradle.properties @@ -1 +1,3 @@ -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +mockk_version=1.12.3 +kotlinx_serialization_version=1.6.0 \ No newline at end of file diff --git a/app-web/src/main/kotlin/com/usvision/web/Application.kt b/app-web/src/main/kotlin/com/usvision/web/Application.kt index 7f4aa9c..6671a26 100644 --- a/app-web/src/main/kotlin/com/usvision/web/Application.kt +++ b/app-web/src/main/kotlin/com/usvision/web/Application.kt @@ -6,10 +6,12 @@ import io.ktor.server.application.Application fun main(args: Array) = io.ktor.server.netty.EngineMain.main(args) fun Application.module() { - val reportSupervisor = configureReports() + val (databaseRepository, databaseAggregateStorage) = configureDatabaseConnection() + val systemCreator = configureSystemCreator(databaseAggregateStorage) + val reportSupervisor = configureReports(databaseRepository) configureCORS() configureSerialization() - configureRouting(reportSupervisor) + configureRouting(reportSupervisor, systemCreator) configureExceptionHandling() } \ No newline at end of file diff --git a/app-web/src/main/kotlin/com/usvision/web/configuration/appcreator.kt b/app-web/src/main/kotlin/com/usvision/web/configuration/appcreator.kt new file mode 100644 index 0000000..407e2ae --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/configuration/appcreator.kt @@ -0,0 +1,9 @@ +package com.usvision.web.configuration + +import com.usvision.creation.SystemAggregateStorage +import com.usvision.creation.SystemCreator +import io.ktor.server.application.* + +fun Application.configureSystemCreator( + systemAggregateStorage: SystemAggregateStorage +) = SystemCreator(systemAggregateStorage) \ No newline at end of file diff --git a/app-web/src/main/kotlin/com/usvision/web/configuration/database.kt b/app-web/src/main/kotlin/com/usvision/web/configuration/database.kt new file mode 100644 index 0000000..d242cb7 --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/configuration/database.kt @@ -0,0 +1,26 @@ +package com.usvision.web.configuration + +import com.usvision.creation.SystemAggregateStorage +import com.usvision.persistence.repositorybuilder.DBRepositoryProvider +import com.usvision.persistence.repositorybuilder.MongoDBRepositoryProvider +import com.usvision.reports.SystemRepository +import io.ktor.server.application.* + +fun Application.configureDatabaseConnection(): Pair { + val host = environment.config.property("persistence.host").getString() + val port = environment.config.property("persistence.port").getString() + val user = environment.config.property("persistence.username").getString() + val pass = environment.config.property("persistence.password").getString() + val dbName = environment.config.property("persistence.database_name").getString() + + val repoProvider: DBRepositoryProvider = MongoDBRepositoryProvider() + + return repoProvider.run { + connectTo(host) + setPort(port) + withCredentials(user, pass) + setDatabase(dbName) + Pair(getRepository(), getAggregateStorage()) + } + +} \ No newline at end of file diff --git a/app-web/src/main/kotlin/com/usvision/web/configuration/reports.kt b/app-web/src/main/kotlin/com/usvision/web/configuration/reports.kt index 17a9806..16bbb00 100644 --- a/app-web/src/main/kotlin/com/usvision/web/configuration/reports.kt +++ b/app-web/src/main/kotlin/com/usvision/web/configuration/reports.kt @@ -1,31 +1,14 @@ package com.usvision.web.configuration -import com.usvision.persistence.repositorybuilder.DBRepositoryProvider -import com.usvision.persistence.repositorybuilder.MongoDBRepositoryProvider import com.usvision.reports.ReportSupervisor +import com.usvision.reports.SystemRepository import io.ktor.server.application.* import io.ktor.server.config.* -fun Application.configureReports(): ReportSupervisor { - val host = environment.config.property("persistence.host").getString() - val port = environment.config.property("persistence.port").getString() - val user = environment.config.property("persistence.username").getString() - val pass = environment.config.property("persistence.password").getString() - val dbName = environment.config.property("persistence.database_name").getString() - +fun Application.configureReports(systemRepository: SystemRepository): ReportSupervisor { val presets = parsePresetsConfig(environment.config.config("reports.presets")) - val repoProvider: DBRepositoryProvider = MongoDBRepositoryProvider() - - val systemRepository = repoProvider.run { - connectTo(host) - setPort(port) - withCredentials(user, pass) - setDatabase(dbName) - getRepository() - } - return ReportSupervisor( systemRepository, presets = presets diff --git a/app-web/src/main/kotlin/com/usvision/web/configuration/routing.kt b/app-web/src/main/kotlin/com/usvision/web/configuration/routing.kt index cbcad8f..7745f8c 100644 --- a/app-web/src/main/kotlin/com/usvision/web/configuration/routing.kt +++ b/app-web/src/main/kotlin/com/usvision/web/configuration/routing.kt @@ -1,18 +1,118 @@ package com.usvision.web.configuration +import com.usvision.creation.SystemCreator +import com.usvision.model.domain.databases.PostgreSQL import com.usvision.reports.ReportSupervisor +import com.usvision.web.dto.* import com.usvision.web.exceptions.MissingRequiredPathParameterException import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Application.configureRouting(reportSupervisor: ReportSupervisor) { +fun Application.configureRouting(reportSupervisor: ReportSupervisor, systemCreator: SystemCreator) { val defaultPreset = environment.config.property("reports.default_preset_name").getString() routing { - route("/systems/{name}/reports") { - get { + route("/microservices") { + post { + val microservice = call.receive().toMicroservice() + val createMicroserviceResult = systemCreator.createMicroservice(microservice) + + call.respond( + message = createMicroserviceResult.toSystemResponseDTO(), + status = HttpStatusCode.Created + ) + } + post("/{name}/databases") { + val microserviceName: String = call.parameters["name"] + ?: throw MissingRequiredPathParameterException("name", "String") + + val databaseDTO = call.receive() + val addDatabaseResult = systemCreator.addNewDatabaseConnectionToMicroservice(databaseDTO, microserviceName) + + call.respond( + message = addDatabaseResult.toMicroserviceResponseDTO(), + status = HttpStatusCode.Created + ) + } + + post("/{name}/rest-endpoints") { + val microserviceName: String = call.parameters["name"] + ?: throw MissingRequiredPathParameterException("name", "String") + val restEndpoints = call.receive() + + val addOperationsResult = systemCreator.addOperationsToMicroservice( + exposedOperations = restEndpoints.exposedOperations, + consumedOperations = restEndpoints.consumedOperations, + microserviceName = microserviceName + ) + + call.respond( + message = addOperationsResult.toMicroserviceResponseDTO(), + status = HttpStatusCode.Created + ) + } + + post("/{name}/message-channels") { + val microserviceName: String = call.parameters["name"] + ?: throw MissingRequiredPathParameterException("name", "String") + + val messageChannels = call.receive() + + + val addMessageChannelsResult = systemCreator.addMessageChannelsToMicroservice( + publishMessageChannels = messageChannels.publishMessageChannels, + subscribedMessageChannels = messageChannels.subscribedMessageChannels, + microserviceName = microserviceName + ) + + call.respond( + message = addMessageChannelsResult.toMicroserviceResponseDTO(), + status = HttpStatusCode.Created + ) + } + } + + route("/systems") { + post { + val companySystem = call.receive().toCompanySystem() + val createCompanySystemResult = systemCreator.createCompanySystem(companySystem) + + call.respond( + message = createCompanySystemResult.toCompanySystemResponseDTO(), + status = HttpStatusCode.Created + ) + } + + post("/{name}/microservices") { + val systemName: String = call.parameters["name"] + ?: throw MissingRequiredPathParameterException("name", "String") + + val microservice = call.receive().toMicroservice() + val createMicroserviceResult = systemCreator.createMicroservice(microservice, systemName) + + call.respond( + message = createMicroserviceResult.toSystemResponseDTO(), + status = HttpStatusCode.Created + ) + } + + post("/{name}/companySubsystems") { + val systemName: String = call.parameters["name"] + ?: throw MissingRequiredPathParameterException("name", "String") + + val companySystem = call.receive().toCompanySystem() + val createCompanySystemResult = systemCreator.createCompanySystem(companySystem, systemName) + + call.respond( + message = createCompanySystemResult.toCompanySystemResponseDTO(), + status = HttpStatusCode.Created + ) + } + + get("/{name}/reports") { val systemName: String = call.parameters["name"] ?: throw MissingRequiredPathParameterException("name", "String") diff --git a/app-web/src/main/kotlin/com/usvision/web/dto/MessageChannelsRequestDTO.kt b/app-web/src/main/kotlin/com/usvision/web/dto/MessageChannelsRequestDTO.kt new file mode 100644 index 0000000..498669a --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/dto/MessageChannelsRequestDTO.kt @@ -0,0 +1,10 @@ +package com.usvision.web.dto + +import com.usvision.model.domain.MessageChannel +import kotlinx.serialization.Serializable + +@Serializable +data class MessageChannelsRequestDTO( + val publishMessageChannels: List, + val subscribedMessageChannels: List +) diff --git a/app-web/src/main/kotlin/com/usvision/web/dto/RestEndpointsRequestDTO.kt b/app-web/src/main/kotlin/com/usvision/web/dto/RestEndpointsRequestDTO.kt new file mode 100644 index 0000000..138caf3 --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/dto/RestEndpointsRequestDTO.kt @@ -0,0 +1,10 @@ +package com.usvision.web.dto + +import com.usvision.model.domain.operations.RestEndpoint +import kotlinx.serialization.Serializable + +@Serializable +data class RestEndpointsRequestDTO( + val exposedOperations: List, + val consumedOperations: List +) diff --git a/app-web/src/main/kotlin/com/usvision/web/dto/SystemRequestDTO.kt b/app-web/src/main/kotlin/com/usvision/web/dto/SystemRequestDTO.kt new file mode 100644 index 0000000..de78f92 --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/dto/SystemRequestDTO.kt @@ -0,0 +1,52 @@ +package com.usvision.web.dto + +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.Microservice +import com.usvision.model.domain.Module +import com.usvision.model.domain.databases.PostgreSQL +import com.usvision.model.domain.operations.RestEndpoint +import com.usvision.model.systemcomposite.System +import kotlinx.serialization.Serializable + +@Serializable +data class SystemRequestDTO( + val name: String, + val subsystems: Set? = null, + val module: Module? = null, + val exposedOperations: MutableSet? = null, + val consumedOperations: MutableSet? = null, + val databases: MutableSet? = null, + val publishChannels: MutableSet? = null, + val subscribedChannels: MutableSet? = null, +) { + fun toMicroservice(): Microservice { + val microservice = module?.let { Microservice(name, module) } ?: Microservice(name) + + databases?.forEach { database -> microservice.addDatabaseConnection(database) } + + exposedOperations?.forEach { operation -> microservice.exposeOperation(operation) } + consumedOperations?.forEach { operation -> microservice.consumeOperation(operation) } + + publishChannels?.forEach { messageChannel -> microservice.addPublishChannel(messageChannel) } + subscribedChannels?.forEach { messageChannel -> microservice.addSubscribedChannel(messageChannel) } + + return microservice + } + + fun toCompanySystem(): CompanySystem { + val companySystem = CompanySystem(name) + + subsystems?.forEach { subsystemRequestDTO -> + companySystem.addSubsystem(subsystemRequestDTO.toSystem()) + } + + return companySystem + } + + private fun toSystem(): System { + subsystems ?: return this.toCompanySystem() + + return this.toMicroservice() + } +} diff --git a/app-web/src/main/kotlin/com/usvision/web/dto/SystemResponseDTO.kt b/app-web/src/main/kotlin/com/usvision/web/dto/SystemResponseDTO.kt new file mode 100644 index 0000000..ad2e902 --- /dev/null +++ b/app-web/src/main/kotlin/com/usvision/web/dto/SystemResponseDTO.kt @@ -0,0 +1,77 @@ +package com.usvision.web.dto + +import com.usvision.model.domain.CompanySystem +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.Microservice +import com.usvision.model.domain.Module +import com.usvision.model.domain.databases.Database +import com.usvision.model.domain.databases.PostgreSQL +import com.usvision.model.domain.operations.Operation +import com.usvision.model.domain.operations.RestEndpoint +import com.usvision.model.exceptions.UnknownOperationClassException +import com.usvision.model.exceptions.UnknownSystemClassException +import com.usvision.model.exceptions.UnknownDatabaseClassException +import com.usvision.model.systemcomposite.System +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +sealed interface SystemResponseDTO + +@SerialName("CompanySystem") +@Serializable +data class CompanySystemResponseDTO( + val name: String, + val subsystems: Set? = setOf(), +): SystemResponseDTO + +@SerialName("Microservice") +@Serializable +data class MicroserviceResponseDTO( + val name: String, + val module: Module, + val exposedOperations: Set, + val consumedOperations: Set, + val databases: Set, + val publishChannels: Set, + val subscribedChannels: Set +): SystemResponseDTO + +fun CompanySystem.toCompanySystemResponseDTO() = CompanySystemResponseDTO( + this.name, + this.getSubsystemSet().toSystemResponseDTOSet() +) + +fun Microservice.toMicroserviceResponseDTO() = MicroserviceResponseDTO( + name = this.name, + module = this.module, + exposedOperations = this.getExposedOperations().toRestEndpointSet(), + consumedOperations = this.getConsumedOperations().toRestEndpointSet(), + databases = this.getDatabases().toPostgreSQLSet(), + publishChannels = this.getPublishChannels(), + subscribedChannels = this.getSubscribedChannels() +) + +fun System.toSystemResponseDTO(): SystemResponseDTO = when (this) { + is Microservice -> this.toMicroserviceResponseDTO() + is CompanySystem -> this.toCompanySystemResponseDTO() + else -> throw UnknownSystemClassException(this.name, "SystemResponseDTO") +} + +private fun Set.toPostgreSQLSet(): Set = this.map { database -> + when (database) { + is PostgreSQL -> database + else -> throw UnknownDatabaseClassException("SystemResponseDTO") + } +}.toSet() + +private fun Set.toRestEndpointSet(): Set = this.map { operation -> + when (operation) { + is RestEndpoint -> operation + else -> throw UnknownOperationClassException("SystemResponseDTO") + } +}.toSet() + +private fun Set.toSystemResponseDTOSet(): Set = this.map { it.toSystemResponseDTO() }.toSet() + diff --git a/app-web/src/test/kotlin/com/usvision/web/configuration/ApplicationRoutingTest.kt b/app-web/src/test/kotlin/com/usvision/web/configuration/ApplicationRoutingTest.kt new file mode 100644 index 0000000..c131f08 --- /dev/null +++ b/app-web/src/test/kotlin/com/usvision/web/configuration/ApplicationRoutingTest.kt @@ -0,0 +1,316 @@ +package com.usvision.web.configuration + +import com.usvision.creation.CompanySystemDTO +import com.usvision.creation.SystemCreator +import com.usvision.model.domain.MessageChannel +import com.usvision.model.domain.Microservice +import com.usvision.model.domain.databases.PostgreSQL +import com.usvision.model.domain.operations.RestEndpoint +import com.usvision.reports.ReportSupervisor +import com.usvision.web.dto.* +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.server.testing.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.plugins.contentnegotiation.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlin.test.* + +internal class ApplicationRoutingTest { + private val systemCreator = mockk() + private val reportSupervisor = mockk() + + @Test + fun `it creates a new microservice`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val microserviceName = "microserviceName" + + val microserviceRequest = SystemRequestDTO( + name = microserviceName + ) + + val createdMicroservice = Microservice(name = microserviceName) + + val expectedResponseBody = createdMicroservice.toMicroserviceResponseDTO() + + every { systemCreator.createMicroservice(any()) } returns createdMicroservice + + val response = client.postRequest( + "/microservices", + microserviceRequest + ) + + val responseBody: SystemResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.createMicroservice(createdMicroservice) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + @Test + fun `it adds a database to a microservice`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val microserviceName = "microserviceName" + + val databaseRequest = PostgreSQL() + + val updatedMicroservice = Microservice( + name = microserviceName + ).apply { + addDatabaseConnection(databaseRequest) + } + + val expectedResponseBody = updatedMicroservice.toMicroserviceResponseDTO() + + every { systemCreator.addNewDatabaseConnectionToMicroservice(any(), any()) } returns updatedMicroservice + + val response = client.postRequest( + "/microservices/$microserviceName/databases", + databaseRequest + ) + + val responseBody: MicroserviceResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.addNewDatabaseConnectionToMicroservice(databaseRequest, microserviceName) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + @Test + fun `it adds rest endpoints to a microservice`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val microserviceName = "microserviceName" + + val restEndpointsRequest = RestEndpointsRequestDTO( + consumedOperations = listOf(RestEndpoint("GET", "/test")), + exposedOperations = listOf(RestEndpoint("POST", "/test")) + ) + + val updatedMicroservice = Microservice( + name = microserviceName + ).apply { + consumeOperation(RestEndpoint("GET", "/test")) + exposeOperation(RestEndpoint("POST", "/test")) + } + + val expectedResponseBody = updatedMicroservice.toMicroserviceResponseDTO() + + every { systemCreator.addOperationsToMicroservice(any(), any(), any()) } returns updatedMicroservice + + val response = client.postRequest( + "/microservices/$microserviceName/rest-endpoints", + restEndpointsRequest + ) + + val responseBody: MicroserviceResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.addOperationsToMicroservice( + consumedOperations = restEndpointsRequest.consumedOperations, + exposedOperations = restEndpointsRequest.exposedOperations, + microserviceName = microserviceName + ) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + @Test + fun `it adds message channels to a microservice`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val microserviceName = "microserviceName" + + val messageChannelsRequest = MessageChannelsRequestDTO( + publishMessageChannels = listOf(MessageChannel("publish")), + subscribedMessageChannels = listOf(MessageChannel("subscribe")) + ) + + val updatedMicroservice = Microservice( + name = microserviceName + ).apply { + addPublishChannel(MessageChannel("publish")) + addSubscribedChannel(MessageChannel("subscribe")) + } + + val expectedResponseBody = updatedMicroservice.toMicroserviceResponseDTO() + + every { systemCreator.addMessageChannelsToMicroservice(any(), any(), any()) } returns updatedMicroservice + + val response = client.postRequest( + "/microservices/$microserviceName/message-channels", + messageChannelsRequest + ) + + val responseBody: MicroserviceResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.addMessageChannelsToMicroservice( + publishMessageChannels = messageChannelsRequest.publishMessageChannels, + subscribedMessageChannels = messageChannelsRequest.subscribedMessageChannels, + microserviceName = microserviceName + ) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + + @Test + fun `it creates a new companySystem`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val companySystemName = "companySystemName" + + val companySystemRequest = SystemRequestDTO( + name = companySystemName + ) + + val createdCompanySystem = CompanySystemDTO(name = companySystemName) + + val expectedResponseBody = createdCompanySystem.toCompanySystemResponseDTO() + + every { systemCreator.createCompanySystem(any()) } returns createdCompanySystem + + val response = client.postRequest( + "/systems", + companySystemRequest + ) + + val responseBody: CompanySystemResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.createCompanySystem(createdCompanySystem) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + @Test + fun `it adds a child microservice to a CompanySystem`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val companySystemName = "companySystemName" + val microserviceName = "microServiceName" + + + val microserviceRequest = SystemRequestDTO( + name = microserviceName + ) + + val createdMicroservice = Microservice(name = microserviceName) + + val createdCompanySystem = CompanySystemDTO(name = companySystemName).apply { + addSubsystem(createdMicroservice) + } + + val expectedResponseBody = createdCompanySystem.toSystemResponseDTO() + + every { systemCreator.createMicroservice(any(), any()) } returns createdCompanySystem + + val response = client.postRequest( + "/systems/$companySystemName/microservices", + microserviceRequest + ) + + val responseBody: SystemResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.createMicroservice(createdMicroservice, companySystemName) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + @Test + fun `it adds a child CompanySystem to a CompanySystem`() = testApplication { + loadUnitTestModules() + val client = getHttpClient() + + val fatherCompanySystemName = "fatherCompanySystemName" + val childCompanySystemName = "childCompanySystemName" + + + val companySystemRequest = SystemRequestDTO( + name = childCompanySystemName + ) + + val createdChildCompanySystem = CompanySystemDTO(name = childCompanySystemName) + + val createdFatherCompanySystem = CompanySystemDTO(name = fatherCompanySystemName).apply { + addSubsystem(createdChildCompanySystem) + } + + val expectedResponseBody = createdFatherCompanySystem.toSystemResponseDTO() + + every { systemCreator.createCompanySystem(any(), any()) } returns createdFatherCompanySystem + + val response = client.postRequest( + "/systems/$fatherCompanySystemName/companySubsystems", + companySystemRequest + ) + + val responseBody: CompanySystemResponseDTO = response.body() + + verify(exactly = 1) { + systemCreator.createCompanySystem(createdChildCompanySystem, fatherCompanySystemName) + } + + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(expectedResponseBody, responseBody) + } + + + private suspend inline fun HttpClient.postRequest( + urlString: String, + body: T + ) = this.post(urlString) { + url { + protocol = URLProtocol.HTTPS + contentType(ContentType.Application.Json) + setBody(body) + } + } + + private fun ApplicationTestBuilder.getHttpClient(): HttpClient { + val client = createClient { + install(ContentNegotiation) { + json() + } + } + return client + } + + private fun ApplicationTestBuilder.loadUnitTestModules() { + application { + configureCORS() + configureSerialization() + configureRouting(reportSupervisor, systemCreator) + configureExceptionHandling() + } + } + +} \ No newline at end of file diff --git a/app-web/src/test/resources/application.yaml b/app-web/src/test/resources/application.yaml new file mode 100644 index 0000000..b8cbef4 --- /dev/null +++ b/app-web/src/test/resources/application.yaml @@ -0,0 +1,6 @@ +ktor: + deployment: + port: 8080 + +reports: + default_preset_name: all \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 27a5d43..3031e56 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,7 @@ services: - ./.env.docker-compose volumes: - mongodb_data:/data/db + - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro ports: - "27017:27017" diff --git a/init-mongo.js b/mongo-init.js similarity index 100% rename from init-mongo.js rename to mongo-init.js diff --git a/settings.gradle.kts b/settings.gradle.kts index 0190d34..d64241d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,4 +4,5 @@ include("app-model") include("app-analyses") include("app-reports") include("app-persistence") -include("app-web") \ No newline at end of file +include("app-web") +include("app-creation")