├── .gitignore ├── .travis.yml ├── README.md ├── build.gradle ├── build.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── kotlin │ └── pl │ │ └── braintelligence │ │ └── projectmanager │ │ ├── Application.kt │ │ ├── core │ │ ├── projects │ │ │ ├── domain │ │ │ │ ├── Feature.kt │ │ │ │ ├── PriorityLevel.kt │ │ │ │ ├── Project.kt │ │ │ │ ├── ProjectCreatorService.kt │ │ │ │ ├── ProjectExceptions.kt │ │ │ │ ├── ProjectFactory.kt │ │ │ │ ├── ProjectQueryService.kt │ │ │ │ ├── Status.kt │ │ │ │ └── configuration │ │ │ │ │ ├── InMemoryProjectRepository.kt │ │ │ │ │ └── TeamConfiguration.kt │ │ │ └── ports │ │ │ │ ├── incoming │ │ │ │ ├── ProjectCreatorPort.kt │ │ │ │ └── ProjectQueryPort.kt │ │ │ │ └── outgoing │ │ │ │ ├── ProjectCreatorRepository.kt │ │ │ │ └── ProjectQueryRepository.kt │ │ └── team │ │ │ ├── domain │ │ │ ├── Employee.kt │ │ │ ├── JobPosition.kt │ │ │ ├── Team.kt │ │ │ ├── TeamExceptions.kt │ │ │ ├── TeamFacade.kt │ │ │ └── configuration │ │ │ │ ├── InMemoryTeamRepository.kt │ │ │ │ └── TeamConfiguration.kt │ │ │ └── ports │ │ │ ├── incoming │ │ │ └── TeamManager.kt │ │ │ └── outgoing │ │ │ └── TeamRepository.kt │ │ ├── infrastructure │ │ ├── adapter │ │ │ ├── incoming │ │ │ │ └── rest │ │ │ │ │ ├── ProjectController.kt │ │ │ │ │ ├── TeamController.kt │ │ │ │ │ └── dto │ │ │ │ │ ├── ProjectDtos.kt │ │ │ │ │ └── TeamDtos.kt │ │ │ └── outgoing │ │ │ │ └── mongo │ │ │ │ ├── project │ │ │ │ ├── TeamCreatorRepository.kt │ │ │ │ ├── TeamQueryRepository.kt │ │ │ │ └── entities │ │ │ │ │ ├── DbFeature.kt │ │ │ │ │ └── DbProject.kt │ │ │ │ └── team │ │ │ │ ├── MongoTeamRepository.kt │ │ │ │ └── entities │ │ │ │ ├── DbEmployee.kt │ │ │ │ └── DbTeam.kt │ │ └── error │ │ │ └── ErrorHandler.kt │ │ └── shared │ │ ├── DomainException.kt │ │ └── InMemoryRepository.kt └── resources │ └── application.yml └── test ├── groovy └── pl │ └── braintelligence │ └── projectmanager │ ├── base │ ├── BaseDtoObjects.groovy │ ├── BaseIntegrationTest.groovy │ ├── BaseUnitTest.groovy │ └── http │ │ └── BaseHttpMethods.groovy │ ├── project │ ├── ProjectAcceptanceTest.groovy │ ├── base │ │ └── BaseProjectUnitTest.groovy │ └── domain │ │ ├── ProjectCreationTest.groovy │ │ └── ProjectQueryTest.groovy │ └── team │ ├── TeamAcceptanceTest.groovy │ ├── base │ └── BaseTeamUnitTest.groovy │ └── domain │ ├── AddingTeamMembersToTeamTest.groovy │ ├── TeamCreationTest.groovy │ ├── TeamMembersValidationTest.groovy │ └── TeamValidationTest.groovy └── kotlin └── pl └── braintelligence └── projectmanager └── core └── team ├── HexagonalArchitectureTest.kt └── NoSpringInDomainTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/web,git,node,java,maven,macos,linux,gradle,kotlin,eclipse,vagrant,netbeans,intellij,archlinuxpackages 3 | # Edit at https://www.gitignore.io/?templates=web,git,node,java,maven,macos,linux,gradle,kotlin,eclipse,vagrant,netbeans,intellij,archlinuxpackages 4 | 5 | ### ArchLinuxPackages ### 6 | *.tar 7 | *.tar.* 8 | *.jar 9 | *.exe 10 | *.msi 11 | *.zip 12 | *.tgz 13 | *.log 14 | *.log.* 15 | *.sig 16 | 17 | pkg/ 18 | src/ 19 | 20 | ### Eclipse ### 21 | .metadata 22 | bin/ 23 | tmp/ 24 | *.tmp 25 | *.bak 26 | *.swp 27 | *~.nib 28 | local.properties 29 | .settings/ 30 | .loadpath 31 | .recommenders 32 | 33 | # External tool builders 34 | .externalToolBuilders/ 35 | 36 | # Locally stored "Eclipse launch configurations" 37 | *.launch 38 | 39 | # PyDev specific (Python IDE for Eclipse) 40 | *.pydevproject 41 | 42 | # CDT-specific (C/C++ Development Tooling) 43 | .cproject 44 | 45 | # CDT- autotools 46 | .autotools 47 | 48 | # Java annotation processor (APT) 49 | .factorypath 50 | 51 | # PDT-specific (PHP Development Tools) 52 | .buildpath 53 | 54 | # sbteclipse plugin 55 | .target 56 | 57 | # Tern plugin 58 | .tern-project 59 | 60 | # TeXlipse plugin 61 | .texlipse 62 | 63 | # STS (Spring Tool Suite) 64 | .springBeans 65 | 66 | # Code Recommenders 67 | .recommenders/ 68 | 69 | # Annotation Processing 70 | .apt_generated/ 71 | 72 | # Scala IDE specific (Scala & Java development for Eclipse) 73 | .cache-main 74 | .scala_dependencies 75 | .worksheet 76 | 77 | ### Eclipse Patch ### 78 | # Eclipse Core 79 | .project 80 | 81 | # JDT-specific (Eclipse Java Development Tools) 82 | .classpath 83 | 84 | # Annotation Processing 85 | .apt_generated 86 | 87 | .sts4-cache/ 88 | 89 | ### Git ### 90 | # Created by git for backups. To disable backups in Git: 91 | # $ git config --global mergetool.keepBackup false 92 | *.orig 93 | 94 | # Created by git when using merge tools for conflicts 95 | *.BACKUP.* 96 | *.BASE.* 97 | *.LOCAL.* 98 | *.REMOTE.* 99 | *_BACKUP_*.txt 100 | *_BASE_*.txt 101 | *_LOCAL_*.txt 102 | *_REMOTE_*.txt 103 | 104 | ### Intellij ### 105 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 106 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 107 | 108 | # User-specific stuff 109 | .idea/**/workspace.xml 110 | .idea/**/tasks.xml 111 | .idea/**/usage.statistics.xml 112 | .idea/**/dictionaries 113 | .idea/**/shelf 114 | 115 | # Generated files 116 | .idea/**/contentModel.xml 117 | 118 | # Sensitive or high-churn files 119 | .idea/**/dataSources/ 120 | .idea/**/dataSources.ids 121 | .idea/**/dataSources.local.xml 122 | .idea/**/sqlDataSources.xml 123 | .idea/**/dynamic.xml 124 | .idea/**/uiDesigner.xml 125 | .idea/**/dbnavigator.xml 126 | 127 | # Gradle 128 | .idea/**/gradle.xml 129 | .idea/**/libraries 130 | 131 | # Gradle and Maven with auto-import 132 | # When using Gradle or Maven with auto-import, you should exclude module files, 133 | # since they will be recreated, and may cause churn. Uncomment if using 134 | # auto-import. 135 | # .idea/modules.xml 136 | # .idea/*.iml 137 | # .idea/modules 138 | 139 | # CMake 140 | cmake-build-*/ 141 | 142 | # Mongo Explorer plugin 143 | .idea/**/mongoSettings.xml 144 | 145 | # File-based project format 146 | *.iws 147 | 148 | # IntelliJ 149 | out/ 150 | 151 | # mpeltonen/sbt-idea plugin 152 | .idea_modules/ 153 | 154 | # JIRA plugin 155 | atlassian-ide-plugin.xml 156 | 157 | # Cursive Clojure plugin 158 | .idea/replstate.xml 159 | 160 | # Crashlytics plugin (for Android Studio and IntelliJ) 161 | com_crashlytics_export_strings.xml 162 | crashlytics.properties 163 | crashlytics-build.properties 164 | fabric.properties 165 | 166 | # Editor-based Rest Client 167 | .idea/httpRequests 168 | 169 | # Android studio 3.1+ serialized cache file 170 | .idea/caches/build_file_checksums.ser 171 | 172 | ### Intellij Patch ### 173 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 174 | 175 | # *.iml 176 | # modules.xml 177 | # .idea/misc.xml 178 | # *.ipr 179 | 180 | # Sonarlint plugin 181 | .idea/sonarlint 182 | 183 | ### Java ### 184 | # Compiled class file 185 | *.class 186 | 187 | # Log file 188 | 189 | # BlueJ files 190 | *.ctxt 191 | 192 | # Mobile Tools for Java (J2ME) 193 | .mtj.tmp/ 194 | 195 | # Package Files # 196 | *.war 197 | *.nar 198 | *.ear 199 | *.tar.gz 200 | *.rar 201 | 202 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 203 | hs_err_pid* 204 | 205 | ### Kotlin ### 206 | # Compiled class file 207 | 208 | # Log file 209 | 210 | # BlueJ files 211 | 212 | # Mobile Tools for Java (J2ME) 213 | 214 | # Package Files # 215 | 216 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 217 | 218 | ### Linux ### 219 | *~ 220 | 221 | # temporary files which can be created if a process still has a handle open of a deleted file 222 | .fuse_hidden* 223 | 224 | # KDE directory preferences 225 | .directory 226 | 227 | # Linux trash folder which might appear on any partition or disk 228 | .Trash-* 229 | 230 | # .nfs files are created when an open file is removed but is still being accessed 231 | .nfs* 232 | 233 | ### macOS ### 234 | # General 235 | .DS_Store 236 | .AppleDouble 237 | .LSOverride 238 | 239 | # Icon must end with two \r 240 | Icon 241 | 242 | # Thumbnails 243 | ._* 244 | 245 | # Files that might appear in the root of a volume 246 | .DocumentRevisions-V100 247 | .fseventsd 248 | .Spotlight-V100 249 | .TemporaryItems 250 | .Trashes 251 | .VolumeIcon.icns 252 | .com.apple.timemachine.donotpresent 253 | 254 | # Directories potentially created on remote AFP share 255 | .AppleDB 256 | .AppleDesktop 257 | Network Trash Folder 258 | Temporary Items 259 | .apdisk 260 | 261 | ### Maven ### 262 | target/ 263 | pom.xml.tag 264 | pom.xml.releaseBackup 265 | pom.xml.versionsBackup 266 | pom.xml.next 267 | release.properties 268 | dependency-reduced-pom.xml 269 | buildNumber.properties 270 | .mvn/timing.properties 271 | .mvn/wrapper/maven-wrapper.jar 272 | 273 | ### NetBeans ### 274 | **/nbproject/private/ 275 | **/nbproject/Makefile-*.mk 276 | **/nbproject/Package-*.bash 277 | build/ 278 | nbbuild/ 279 | dist/ 280 | nbdist/ 281 | .nb-gradle/ 282 | 283 | ### Node ### 284 | # Logs 285 | logs 286 | npm-debug.log* 287 | yarn-debug.log* 288 | yarn-error.log* 289 | lerna-debug.log* 290 | 291 | # Diagnostic reports (https://nodejs.org/api/report.html) 292 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 293 | 294 | # Runtime data 295 | pids 296 | *.pid 297 | *.seed 298 | *.pid.lock 299 | 300 | # Directory for instrumented libs generated by jscoverage/JSCover 301 | lib-cov 302 | 303 | # Coverage directory used by tools like istanbul 304 | coverage 305 | 306 | # nyc test coverage 307 | .nyc_output 308 | 309 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 310 | .grunt 311 | 312 | # Bower dependency directory (https://bower.io/) 313 | bower_components 314 | 315 | # node-waf configuration 316 | .lock-wscript 317 | 318 | # Compiled binary addons (https://nodejs.org/api/addons.html) 319 | build/Release 320 | 321 | # Dependency directories 322 | node_modules/ 323 | jspm_packages/ 324 | 325 | # TypeScript v1 declaration files 326 | typings/ 327 | 328 | # Optional npm cache directory 329 | .npm 330 | 331 | # Optional eslint cache 332 | .eslintcache 333 | 334 | # Optional REPL history 335 | .node_repl_history 336 | 337 | # Output of 'npm pack' 338 | 339 | # Yarn Integrity file 340 | .yarn-integrity 341 | 342 | # dotenv environment variables file 343 | .env 344 | .env.test 345 | 346 | # parcel-bundler cache (https://parceljs.org/) 347 | .cache 348 | 349 | # next.js build output 350 | .next 351 | 352 | # nuxt.js build output 353 | .nuxt 354 | 355 | # vuepress build output 356 | .vuepress/dist 357 | 358 | # Serverless directories 359 | .serverless/ 360 | 361 | # FuseBox cache 362 | .fusebox/ 363 | 364 | # DynamoDB Local files 365 | .dynamodb/ 366 | 367 | ### Vagrant ### 368 | # General 369 | .vagrant/ 370 | 371 | # Log files (if you are creating logs in debug mode, uncomment this) 372 | # *.logs 373 | 374 | ### Vagrant Patch ### 375 | *.box 376 | 377 | ### Web ### 378 | *.asp 379 | *.cer 380 | *.csr 381 | *.css 382 | *.htm 383 | *.html 384 | *.js 385 | *.jsp 386 | *.php 387 | *.rss 388 | *.xhtml 389 | 390 | ### Gradle ### 391 | .gradle 392 | 393 | # Ignore Gradle GUI config 394 | gradle-app.setting 395 | 396 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 397 | !gradle-wrapper.jar 398 | 399 | # Cache of project 400 | .gradletasknamecache 401 | 402 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 403 | # gradle/wrapper/gradle-wrapper.properties 404 | 405 | ### Gradle Patch ### 406 | **/build/ 407 | 408 | # End of https://www.gitignore.io/api/web,git,node,java,maven,macos,linux,gradle,kotlin,eclipse,vagrant,netbeans,intellij,archlinuxpackages 409 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk10 5 | 6 | install: ./gradlew clean build 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | #### [![Build Status](https://travis-ci.com/braintelligencePL/project-manager-kotlin.svg?branch=master)](https://travis-ci.com/braintelligencePL/project-manager-kotlin) 🛠 3 | 4 | ### [Project-Manager - just like trello but only backend](https://github.com/braintelligencePL/project-manager-kotlin) 5 | #### Journey from layered (n-tier) architecture to hexagonal architecture in Kotlin 💪 6 | Project-Manager is a simple application for managing projects at company. 7 | You can create projects and teams. 8 | Projects can have features and status. 9 | Teams can have members and projects. 10 | You'll find more business requirments below. 11 | 12 | We'll go from traditional layered architecture to hexagonal architecture A.K.A. Ports and Adapters architecture. 13 | 14 |
15 | 16 | ### **There is also newer repository with similar architecture style:** 17 | 18 | #### [Online Store - clean architecture](https://github.com/braintelligencePL/online-store-clean-architecture) 19 | 20 |
21 | 22 | # Quick Start 23 | 24 | ### Working with Project-Manager 25 | 26 | `./gradlew bootRun` - to run application.
27 | `./gradlew test` - to run unit tests.
28 | `./gradlew clean build test`- one to rule them all 💍
29 |
30 | 31 | Start with [endpoints](https://github.com/braintelligencePL/project-manager-kotlin/tree/master/src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/incoming/rest). 32 | After that check [tests](https://github.com/braintelligencePL/project-manager-kotlin/tree/master/src/test/groovy/pl/braintelligence/projectmanager). Whole domain is tested with unit tests. Isolated from controllers, database, framework. Tests are done with a use of repository implemented as HashMap. You also have [AcceptanceTests](https://github.com/braintelligencePL/project-manager-kotlin/blob/master/src/test/groovy/pl/braintelligence/projectmanager/project/ProjectAcceptanceTest.groovy) that show user flow, bigger picture. 33 | 34 |
35 | 36 | ## Implementation step-by-step 37 | 38 | Idea is to see how does project changes while time passes. Each branch has some changes either refactor or new features implemented. 39 | 40 |
41 | 42 | ### 1️⃣ `branch: step-1-team`
43 | 🏠 **Architecture**: Layered Architecure
44 | 45 | * [x] `POST: /teams` - create a team.
46 | * [x] `POST: /teams/:teamName/members` - add members to the team.
47 | * [x] `GET: /teams` - show teams.

48 | 49 | Needs and constraints: 50 | * Team cannot be created if already exists 51 | * How many projects team has? 52 | 53 | 54 |
55 | 56 | ### 2️⃣ `branch: step-2-projects`
57 | 🏠 **Architecture**: Layered Architecure
58 | 59 | * [x] `POST: /projects/drafts` - create project draft (only project name).
60 | * [x] `POST: /projects` - create project with features. 📊
61 | * [x] `GET: /projects` - show draft projects
62 | 63 | Needs and constraints: 64 | * JobPosition must be valid (Developer, Scrum Master...) 65 | * Team can have no more than 3 projects at the time 66 | 67 |
68 | 69 | ### 3️⃣ `branch: step-3-refactor`
70 | 🏠 **Architecture**: Hexagonal Architecure
71 | 72 | Things done: 73 | 74 | * Moving from layered (n-tier) architecture to Hexagonal Architecture (ports and adapters). 😎 75 | * Introduced idea of shared-kernel from DDD 76 | 77 | Improved tests: 78 | 79 | * Unit tests without touching IO. Domain is tested with unit tests. Idea of `InMemoryRepository` as HashMap. 80 | * Acceptance tests show flow of the app or bounded-context. 81 | * Integration tests are only for most important business value paths because whole domain is tested with unit tests. 82 | 83 |
84 | 85 | ### 4️⃣ `branch: step-4-projects`
86 | 🏠 **Architecture**: Hexagonal Architecure
87 | 88 | * [ ] `GET: /projects/:id` - show project
89 | * [ ] `GET: /projects` - show projects
90 | * [ ] `PUT: /projects/:id` - change/update project
91 | * [ ] `PATCH: /projects/:id/started` - start project when team assigned
92 | * [ ] `PATCH: /projects/:id/ended` - close project when features are done

93 | 94 | Needs and constraints: 95 | * No `if` statements! We can do better in Kotlin. Not something that you should avoid at any cost (its just a kata). 96 | * Project status and feature status -> `Status` must be valid (TO_DO, IN_PROGRESS...) 97 | * `PriorityLevel` for project features must be valid (HIGH, MEDIUM, NOT_DEFINED...) 98 | 99 |
100 | 101 | ### #️⃣ `branch: will-be-more`
102 | - Refactor introducing simple CQRS. 103 | - monitoring, grafana, actuator, performance tests with gatling 104 | 105 | `branch: step-X-zoo-of-microservices`
106 | 107 | 🏠 **Architecture**: Hexagonal Architecture (modularization on package level)
108 | 🕳 **Tests**: Integration/Acceptance/Unit
109 | Backing-Services from [Twelve-Factor-App](https://12factor.net/) methodology. 110 | 111 | Services from our zoo:
112 | 🦓 **user-autorization-service** - authentication gateway that protects back-end resources. There is two kinds of resources protected and unprotected. First one requires user-level authentication and second one is just read-only such as listing of offers/products.

113 | 🐼 **edge-service** - gives possibility to expose unified REST API from all of ours backend services. To be able to do this, the Edge Service matches a request route’s URL fragment from a front-end application to a back-end microservice through a reverse proxy to retrieve the remote REST API response.

114 | 🐰 **discovery-service** - Edge-service matches a request route’s URL fragment from a front-end application to a back-end microservice through a reverse proxy to retrieve the remote REST API response.

115 | 🐿 **centralized-configuration-server** - Spring Cloud application that centralizes external configurations using various methodologies of [building twelve-factor applications](https://12factor.net/config).

116 | 117 |
118 | 119 | ## Technologies used: 120 | - Kotlin with spring 121 | - Spock (groovy) for tests 122 | - ArchUnit (kotlin) for architecture tests 123 | - Gradle to build project 124 | 125 | ### Materials from mine blog: 126 | * PL: [Prawie trywialna aplikacja do zarządzania projektami](http://braintelligence.pl/prawie-trywialna-aplikacja-do-zarzadzania-projektami) - bardziej szczegółowy opis projektu. 127 | * ENG: [ The nature of domain driven design](http://www.braintelligence.pl/the-nature-of-domain-driven-design/) - about DDD strategic tools. 128 | 129 | ### Materials from outside world: 130 | * ENG: [ ddd-workshop-project-manager (business requirments from this repo)](https://github.com/mkopylec/project-manager) 131 | * ENG: [ example of hexagonal architecture (on package level)](https://github.com/jakubnabrdalik/hentai) 132 | * ENG: [ design patterns for humans ](https://github.com/kamranahmedse/design-patterns-for-humans) 133 | * ENG: [ ddd-laeven ](https://github.com/BottegaIT/ddd-leaven-v2) 134 | * ENG: [ awsome-ddd ](https://github.com/heynickc/awesome-ddd) 135 | * ENG: [ twelve-factor-app - methodology for building software-as-a-service](https://12factor.net/) 136 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.3.21' 4 | springBootVersion = '2.1.2.RELEASE' 5 | } 6 | repositories { 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 12 | // classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion" 13 | } 14 | } 15 | 16 | apply plugin: 'idea' 17 | apply plugin: 'kotlin' 18 | apply plugin: 'groovy' 19 | //apply plugin: 'kotlin-spring' 20 | apply plugin: 'org.springframework.boot' 21 | apply plugin: 'io.spring.dependency-management' 22 | 23 | repositories { 24 | jcenter() 25 | mavenCentral() 26 | maven { url 'https://jitpack.io' } // for Spock 1.3 27 | } 28 | 29 | group = 'pl.braintelligence' 30 | sourceCompatibility = JavaVersion.VERSION_1_10 31 | targetCompatibility = JavaVersion.VERSION_1_10 32 | 33 | dependencies { 34 | // Kotlin 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 36 | implementation "org.jetbrains.kotlin:kotlin-reflect" 37 | implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+" 38 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' 39 | implementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:2.2.0' 40 | implementation 'io.arrow-kt:arrow-core:0.8.2' 41 | 42 | // Spring 43 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 44 | implementation 'org.springframework.boot:spring-boot-starter-web' 45 | implementation "org.springframework.boot:spring-boot-starter-data-mongodb" 46 | 47 | // Commons, utils 48 | implementation 'org.apache.commons:commons-lang3:3.7' 49 | implementation 'org.apache.commons:commons-collections4:4.1' 50 | implementation 'org.apache.httpcomponents:httpclient:4.5.6' 51 | implementation "io.vavr:vavr:0.9.3" 52 | 53 | // Tests 54 | testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5' 55 | testImplementation 'org.spockframework:spock-spring:1.3-groovy-2.5' 56 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 57 | 58 | testImplementation 'com.github.tomakehurst:wiremock:2.21.0' 59 | testImplementation 'com.tngtech.archunit:archunit:0.10.1' 60 | 61 | testImplementation 'com.tngtech.archunit:archunit-junit5-api:0.9.3' 62 | testImplementation 'com.tngtech.archunit:archunit-junit5-engine:0.9.3' 63 | testImplementation 'org.junit.jupiter:junit-jupiter-api' 64 | testImplementation 'org.junit.jupiter:junit-jupiter-params' 65 | testImplementation group: 'org.awaitility', name: 'awaitility-groovy', version: '3.1.0' 66 | testRuntime 'org.junit.jupiter:junit-jupiter-engine' 67 | testRuntime 'org.junit.platform:junit-platform-engine' 68 | } 69 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ./gradlew clean check assemble 4 | docker build -t pl.braintelligence -f docker/prometheus/Dockerfile . 5 | docker build -t pl.braintelligence -f docker/app/Dockerfile . 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braintelligencePL/project-manager-kotlin/21164747c0e639b3d9ef30ed9e6ff445ec31d260/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Mar 31 09:19:24 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.3.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'project-manager-kotlin' -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/Application.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager 2 | 3 | import org.slf4j.Logger 4 | import org.slf4j.LoggerFactory 5 | import org.springframework.boot.SpringApplication 6 | import org.springframework.boot.autoconfigure.SpringBootApplication 7 | import kotlin.reflect.full.companionObject 8 | 9 | 10 | @SpringBootApplication 11 | open class Application 12 | 13 | fun main(args: Array) { 14 | SpringApplication.run(Application::class.java, *args) 15 | } 16 | 17 | fun R.logger(): Lazy = lazy { LoggerFactory.getLogger(unwrapCompanionClass(this.javaClass).name) } 18 | 19 | fun unwrapCompanionClass(ofClass: Class): Class<*> = 20 | if (ofClass.enclosingClass?.kotlin?.companionObject?.java == ofClass) { 21 | ofClass.enclosingClass 22 | } else { 23 | ofClass 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/Feature.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | class Feature( 4 | val name: String, 5 | val status: Status = Status.TO_DO, 6 | val priorityLevel: PriorityLevel = PriorityLevel.NOT_DEFINED 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/PriorityLevel.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | enum class PriorityLevel { 4 | HIGHEST, 5 | HIGH, 6 | MEDIUM, 7 | LOW, 8 | LOWEST, 9 | NOT_DEFINED 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/Project.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | data class Project( 4 | val id: String, 5 | val name: String, 6 | val status: Status = Status.TO_DO, 7 | val teamAssigned: String = "", 8 | val features: List = listOf() 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/ProjectCreatorService.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | import org.springframework.stereotype.Service 4 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectCreatorPort 5 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectCreatorRepository 6 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectDraft 7 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectWithFeatures 8 | 9 | @Service 10 | class ProjectCreatorService( 11 | private val projectFactory: ProjectFactory, 12 | private val projectCreatorRepository: ProjectCreatorRepository 13 | ) : ProjectCreatorPort { 14 | 15 | override fun createProjectDraft(projectDraft: ProjectDraft): Project = 16 | projectFactory.createProjectDraft(projectDraft.projectName) 17 | .also { projectCreatorRepository.save(it) } 18 | 19 | override fun createProjectWithFeatures(projectWithFeatures: ProjectWithFeatures): Project = 20 | projectFactory.createProjectWithFeatures(projectWithFeatures) 21 | .also { projectCreatorRepository.save(it) } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/ProjectExceptions.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | import pl.braintelligence.projectmanager.shared.DomainException 4 | 5 | 6 | internal class MissingProjectException(message: String) : DomainException(message) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/ProjectFactory.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | import org.springframework.stereotype.Component 4 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectWithFeatures 5 | import java.util.* 6 | 7 | @Component 8 | open class ProjectFactory { 9 | 10 | fun createProjectDraft(projectName: String): Project { 11 | val id = generateProjectUniqueId() 12 | return Project(id = id, name = projectName) 13 | } 14 | 15 | fun createProjectWithFeatures(projectWithFeatures: ProjectWithFeatures): Project { 16 | val id = generateProjectUniqueId() 17 | val name = projectWithFeatures.projectName 18 | val features = projectWithFeatures.features 19 | 20 | return Project(id = id, name = name, features = features) 21 | } 22 | 23 | 24 | private fun generateProjectUniqueId() = UUID.randomUUID().toString() 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/ProjectQueryService.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | import org.springframework.stereotype.Service 4 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectQueryPort 5 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectQueryRepository 6 | 7 | @Service 8 | class ProjectQueryService( 9 | private val projectQueryRepository: ProjectQueryRepository 10 | ) : ProjectQueryPort { 11 | 12 | override fun getProject(id: String): Project = 13 | projectQueryRepository.findById(id) 14 | ?: throw MissingProjectException("Project does not exist.") 15 | 16 | override fun getProjects(): List { 17 | TODO("not implemented") 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/Status.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain 2 | 3 | enum class Status { 4 | TO_DO, 5 | IN_PROGRESS, 6 | COMPLETED 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/configuration/InMemoryProjectRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain.configuration 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Project 4 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectCreatorRepository 5 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectQueryRepository 6 | import pl.braintelligence.projectmanager.shared.InMemoryCrudRepository 7 | 8 | open class InMemoryProjectRepository : InMemoryCrudRepository(), ProjectCreatorRepository, ProjectQueryRepository { 9 | 10 | override fun save(project: Project) { 11 | super.save(entity = project, id = project.id) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/domain/configuration/TeamConfiguration.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.domain.configuration 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import pl.braintelligence.projectmanager.core.projects.domain.ProjectCreatorService 6 | import pl.braintelligence.projectmanager.core.projects.domain.ProjectFactory 7 | import pl.braintelligence.projectmanager.core.projects.domain.ProjectQueryService 8 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectCreatorPort 9 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectQueryPort 10 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectCreatorRepository 11 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectQueryRepository 12 | 13 | @Configuration 14 | open class ProjectConfiguration { 15 | 16 | open fun buildProjectCreator( 17 | projectFactory: ProjectFactory, 18 | inMemoryProjectRepository: InMemoryProjectRepository 19 | ): ProjectCreatorPort = 20 | ProjectCreatorService(projectFactory, inMemoryProjectRepository) 21 | 22 | @Bean 23 | open fun buildProjectCreator( 24 | projectFactory: ProjectFactory, 25 | projectCreatorRepository: ProjectCreatorRepository 26 | ): ProjectCreatorPort = 27 | ProjectCreatorService(projectFactory, projectCreatorRepository) 28 | 29 | 30 | open fun buildProjectQuery( 31 | inMemoryProjectRepository: InMemoryProjectRepository 32 | ): ProjectQueryPort = 33 | ProjectQueryService(inMemoryProjectRepository) 34 | 35 | @Bean 36 | open fun buildProjectQuery( 37 | projectQueryRepository: ProjectQueryRepository 38 | ): ProjectQueryPort = 39 | ProjectQueryService(projectQueryRepository) 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/ports/incoming/ProjectCreatorPort.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.ports.incoming 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Project 4 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectDraft 5 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectWithFeatures 6 | 7 | interface ProjectCreatorPort { 8 | 9 | fun createProjectDraft(projectDraft: ProjectDraft): Project 10 | 11 | fun createProjectWithFeatures(projectWithFeatures: ProjectWithFeatures): Project 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/ports/incoming/ProjectQueryPort.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.ports.incoming 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Project 4 | 5 | interface ProjectQueryPort { 6 | 7 | fun getProject(id: String): Project 8 | 9 | fun getProjects(): List 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/ports/outgoing/ProjectCreatorRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.ports.outgoing 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Project 4 | 5 | interface ProjectCreatorRepository { 6 | 7 | fun save(project: Project) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/projects/ports/outgoing/ProjectQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.projects.ports.outgoing 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Project 4 | 5 | interface ProjectQueryRepository { 6 | 7 | fun findById(id: String): Project? 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/Employee.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain 2 | 3 | import arrow.core.Try 4 | import arrow.core.getOrElse 5 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 6 | 7 | class Employee( 8 | val firstName: String, 9 | val lastName: String, 10 | val jobPosition: JobPosition 11 | ) { 12 | fun hasNoFirstName(): Boolean = firstName.isNotBlank() 13 | fun hasNoLastName(): Boolean = lastName.isNotBlank() 14 | fun hasInvalidJobPosition(): Boolean = jobPosition.isValid() 15 | 16 | companion object { 17 | fun mapToEmployee(teamMember: TeamMember): Employee = 18 | Employee( 19 | teamMember.firstName, 20 | teamMember.lastName, 21 | toJobPosition(teamMember.jobPosition) 22 | ) 23 | 24 | private fun toJobPosition(jobPosition: String) = 25 | Try { JobPosition.valueOf(jobPosition) } 26 | .getOrElse { JobPosition.INVALID } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/JobPosition.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain 2 | 3 | enum class JobPosition { 4 | SCRUM_MASTER, 5 | DEVELOPER, 6 | PRODUCT_OWNER, 7 | INVALID; 8 | 9 | fun isValid(): Boolean { 10 | return this != INVALID 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/Team.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain 2 | 3 | data class Team @JvmOverloads constructor( 4 | val name: String, 5 | val numberOfOngoingProjects: Int = 0, 6 | var members: List = listOf() 7 | ) { 8 | init { 9 | require(name.isNotBlank()) { throw InvalidTeamException("Empty team name.") } 10 | } 11 | 12 | fun addMember(teamMember: Employee) { 13 | validateMember(teamMember) 14 | members = members.plus(teamMember) 15 | } 16 | 17 | fun isTeamBusy(): Boolean = numberOfOngoingProjects > BUSY_TEAM_THRESHOLD 18 | 19 | private fun validateMember(teamMember: Employee) { 20 | require(teamMember.hasNoFirstName()) { throw InvalidTeamMemberException("Empty member first name.") } 21 | require(teamMember.hasNoLastName()) { throw InvalidTeamMemberException("Empty member last name.") } 22 | require(teamMember.hasInvalidJobPosition()) { throw InvalidTeamMemberException("Invalid job position.") } 23 | } 24 | 25 | companion object { 26 | const val BUSY_TEAM_THRESHOLD = 3 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/TeamExceptions.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain 2 | 3 | import pl.braintelligence.projectmanager.shared.DomainException 4 | 5 | 6 | internal class EntityAlreadyExistsException(message: String) : DomainException(message) 7 | 8 | internal class MissingTeamException(message: String) : DomainException(message) 9 | 10 | internal class InvalidTeamException(message: String) : DomainException(message) 11 | 12 | internal class InvalidTeamMemberException(message: String) : DomainException(message) 13 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/TeamFacade.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain 2 | 3 | import org.springframework.stereotype.Service 4 | import pl.braintelligence.projectmanager.core.team.ports.incoming.TeamManager 5 | import pl.braintelligence.projectmanager.core.team.ports.outgoing.TeamRepository 6 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.NewTeam 7 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 8 | 9 | @Service 10 | class TeamFacade( 11 | private val teamRepository: TeamRepository 12 | ) : TeamManager { 13 | 14 | override fun createTeam(newTeam: NewTeam) = when (teamRepository.existsByName(newTeam.name)) { 15 | true -> throw EntityAlreadyExistsException("Team already exist.") 16 | false -> teamRepository.save(Team(name = newTeam.name)) 17 | } 18 | 19 | override fun addMemberToTeam(teamName: String, teamMember: TeamMember) { 20 | val team = getTeam(teamName) 21 | 22 | Employee.mapToEmployee(teamMember) 23 | .apply { team.addMember(this) } 24 | .also { teamRepository.save(team) } 25 | } 26 | 27 | override fun getTeams(): List = 28 | teamRepository.findAll() 29 | 30 | override fun getTeam(teamName: String): Team = 31 | teamRepository.findByName(teamName) 32 | ?: throw MissingTeamException("Team does not exist.") 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/configuration/InMemoryTeamRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain.configuration 2 | 3 | import pl.braintelligence.projectmanager.core.team.domain.Team 4 | import pl.braintelligence.projectmanager.core.team.ports.outgoing.TeamRepository 5 | import pl.braintelligence.projectmanager.shared.InMemoryCrudRepository 6 | 7 | class InMemoryTeamRepository : InMemoryCrudRepository(), TeamRepository { 8 | 9 | override fun existsByName(name: String): Boolean { 10 | return super.contains(id = name) 11 | } 12 | 13 | override fun findByName(name: String): Team? { 14 | return super.findById(id = name) 15 | } 16 | 17 | override fun save(team: Team) { 18 | super.save(entity = team, id = team.name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/domain/configuration/TeamConfiguration.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.domain.configuration 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import pl.braintelligence.projectmanager.core.team.domain.TeamFacade 6 | import pl.braintelligence.projectmanager.core.team.ports.incoming.TeamManager 7 | import pl.braintelligence.projectmanager.core.team.ports.outgoing.TeamRepository 8 | 9 | @Configuration 10 | open class TeamConfiguration { 11 | 12 | open fun teamManager(): TeamManager = 13 | teamManager(InMemoryTeamRepository()) 14 | 15 | @Bean 16 | open fun teamManager(teamRepository: TeamRepository): TeamManager = 17 | TeamFacade(teamRepository) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/ports/incoming/TeamManager.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.ports.incoming 2 | 3 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.NewTeam 4 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 5 | import pl.braintelligence.projectmanager.core.team.domain.Team 6 | 7 | /** 8 | * Primary Port 9 | */ 10 | 11 | interface TeamManager { 12 | 13 | fun createTeam(newTeam: NewTeam) 14 | 15 | fun addMemberToTeam(teamName: String, teamMember: TeamMember) 16 | 17 | fun getTeams(): List 18 | 19 | fun getTeam(teamName: String): Team 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/core/team/ports/outgoing/TeamRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team.ports.outgoing 2 | 3 | import pl.braintelligence.projectmanager.core.team.domain.Team 4 | 5 | /** 6 | * Secondary Port 7 | */ 8 | 9 | interface TeamRepository { 10 | 11 | fun existsByName(name: String): Boolean 12 | 13 | fun findByName(name: String): Team? 14 | 15 | fun findAll(): List 16 | 17 | fun save(team: Team) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/incoming/rest/ProjectController.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier 4 | import org.springframework.http.HttpStatus 5 | import org.springframework.web.bind.annotation.* 6 | import pl.braintelligence.projectmanager.core.projects.domain.Project 7 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectCreatorPort 8 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectQueryPort 9 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectDraft 10 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectWithFeatures 11 | 12 | /** 13 | * Primary Adapter 14 | */ 15 | 16 | @RestController 17 | @RequestMapping("/projects") 18 | internal class ProjectController( 19 | @Qualifier("projectCreatorService") private val projectCreatorPort: ProjectCreatorPort, 20 | @Qualifier("projectQueryService") private val projectQueryPort: ProjectQueryPort 21 | ) { 22 | 23 | @PostMapping("drafts") 24 | @ResponseStatus(HttpStatus.CREATED) 25 | fun createProjectDraft( 26 | @RequestBody projectDraft: ProjectDraft 27 | ): Project = projectCreatorPort.createProjectDraft(projectDraft) 28 | 29 | @PostMapping 30 | @ResponseStatus(HttpStatus.CREATED) 31 | fun createProjectWithFeatures( 32 | @RequestBody projectWithFeatures: ProjectWithFeatures 33 | ): Project = projectCreatorPort.createProjectWithFeatures(projectWithFeatures) 34 | 35 | @GetMapping("/{id}") 36 | @ResponseStatus(HttpStatus.OK) 37 | fun getProject( 38 | @PathVariable id: String 39 | ): Project = projectQueryPort.getProject(id) 40 | 41 | @GetMapping 42 | @ResponseStatus(HttpStatus.OK) 43 | fun getProjects(): List = TODO() 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/incoming/rest/TeamController.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest 2 | 3 | import org.springframework.http.HttpStatus 4 | import org.springframework.web.bind.annotation.* 5 | import pl.braintelligence.projectmanager.core.team.ports.incoming.TeamManager 6 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ExistingTeam 7 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.NewTeam 8 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 9 | 10 | /** 11 | * Primary Adapter 12 | */ 13 | 14 | @RestController 15 | @RequestMapping("/teams") 16 | class TeamController( 17 | private val teamManager: TeamManager 18 | ) { 19 | 20 | @ResponseStatus(HttpStatus.CREATED) 21 | @PostMapping 22 | fun createTeam( 23 | @RequestBody newTeamDto: NewTeam 24 | ) = teamManager.createTeam(newTeamDto) 25 | 26 | @ResponseStatus(HttpStatus.CREATED) 27 | @PostMapping("{teamName}/members") 28 | fun addMemberToTeam( 29 | @PathVariable teamName: String, 30 | @RequestBody teamMember: TeamMember 31 | ) = teamManager.addMemberToTeam(teamName, teamMember) 32 | 33 | @ResponseStatus(HttpStatus.OK) 34 | @GetMapping 35 | fun getTeams(): List = 36 | ExistingTeam.toExistingTeams(teamManager.getTeams()) 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/incoming/rest/dto/ProjectDtos.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Feature 4 | 5 | data class ProjectDraft(val projectName: String) 6 | 7 | data class ProjectWithFeatures(val projectName: String, val features: List) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/incoming/rest/dto/TeamDtos.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto 2 | 3 | import pl.braintelligence.projectmanager.core.team.domain.Employee 4 | import pl.braintelligence.projectmanager.core.team.domain.Team 5 | 6 | data class NewTeam(val name: String) 7 | 8 | data class TeamMember( 9 | val firstName: String, 10 | val lastName: String, 11 | val jobPosition: String 12 | ) { 13 | companion object { 14 | fun toTeamMembers(employees: List): List = 15 | employees.map { 16 | TeamMember( 17 | it.firstName, 18 | it.lastName, 19 | it.jobPosition.toString()) 20 | } 21 | } 22 | } 23 | 24 | data class ExistingTeam( 25 | val name: String, 26 | val currentlyImplementedProjects: Int, 27 | val members: List, 28 | val isTeamBusy: Boolean 29 | ) { 30 | companion object { 31 | fun toExistingTeams(teams: List): List = 32 | teams.map { 33 | ExistingTeam( 34 | it.name, 35 | it.numberOfOngoingProjects, 36 | TeamMember.toTeamMembers(it.members), 37 | it.isTeamBusy()) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/project/TeamCreatorRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project 2 | 3 | import org.springframework.data.repository.CrudRepository 4 | import org.springframework.stereotype.Component 5 | import org.springframework.stereotype.Repository 6 | import pl.braintelligence.projectmanager.core.projects.domain.Project 7 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectCreatorRepository 8 | import pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project.entities.DbProject 9 | 10 | @Repository 11 | interface MongoTeamCreationRepository : CrudRepository 12 | 13 | @Component 14 | class TeamCreatorRepository( 15 | private val mongo: MongoTeamCreationRepository 16 | ) : ProjectCreatorRepository { 17 | 18 | override fun save(project: Project) { 19 | val dbProject = DbProject.toDbProject(project) 20 | mongo.save(dbProject) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/project/TeamQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project 2 | 3 | import org.springframework.data.mongodb.repository.MongoRepository 4 | import org.springframework.stereotype.Component 5 | import org.springframework.stereotype.Repository 6 | import pl.braintelligence.projectmanager.core.projects.domain.Project 7 | import pl.braintelligence.projectmanager.core.projects.ports.outgoing.ProjectQueryRepository 8 | import pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project.entities.DbProject 9 | 10 | @Repository 11 | interface MongoTeamQueryRepository : MongoRepository 12 | 13 | @Component 14 | class TeamQueryRepository( 15 | private val mongo: MongoTeamCreationRepository 16 | ) : ProjectQueryRepository { 17 | 18 | override fun findById(id: String): Project? = DbProject.toProject(mongo.findById(id).get()) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/project/entities/DbFeature.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project.entities 2 | 3 | data class DbFeature( 4 | val name: String, 5 | val status: String, 6 | val priorityLevel: String 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/project/entities/DbProject.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.project.entities 2 | 3 | import arrow.core.Try 4 | import arrow.core.getOrElse 5 | import org.springframework.data.annotation.Id 6 | import org.springframework.data.mongodb.core.mapping.Document 7 | import pl.braintelligence.projectmanager.core.projects.domain.Feature 8 | import pl.braintelligence.projectmanager.core.projects.domain.PriorityLevel 9 | import pl.braintelligence.projectmanager.core.projects.domain.Project 10 | import pl.braintelligence.projectmanager.core.projects.domain.Status 11 | 12 | @Document(collection = "projects") 13 | class DbProject( 14 | @Id val id: String, 15 | val name: String, 16 | val status: Status = Status.TO_DO, 17 | val teamAssigned: String = "", 18 | val features: List = listOf() 19 | ) { 20 | companion object { 21 | fun toDbProject(project: Project): DbProject = DbProject( 22 | project.id, 23 | project.name, 24 | project.status, 25 | project.teamAssigned, 26 | project.features.map { 27 | DbFeature( 28 | name = it.name, 29 | status = it.status.toString(), 30 | priorityLevel = it.priorityLevel.toString() 31 | ) 32 | } 33 | ) 34 | 35 | fun toProject(dbProject: DbProject): Project = Project( 36 | dbProject.id, 37 | dbProject.name, 38 | dbProject.status, 39 | dbProject.teamAssigned, 40 | dbProject.features.map { 41 | Feature( 42 | name = it.name, 43 | status = Try { Status.valueOf(it.status) } 44 | .getOrElse { throw IllegalArgumentException("Not valid Feature status") }, 45 | priorityLevel = Try { PriorityLevel.valueOf(it.priorityLevel) } 46 | .getOrElse { throw IllegalArgumentException("Not valid Feature priorityLevel") } 47 | ) 48 | } 49 | 50 | ) 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/team/MongoTeamRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.adapter.outgoing.mongo.team 2 | 3 | import DbTeam 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Component 6 | import org.springframework.stereotype.Repository 7 | import pl.braintelligence.projectmanager.core.team.domain.Team 8 | import pl.braintelligence.projectmanager.core.team.ports.outgoing.TeamRepository 9 | 10 | 11 | @Repository 12 | interface CrudTeamRepository : CrudRepository { 13 | fun findByName(name: String): DbTeam? 14 | } 15 | 16 | /** 17 | * Secondary adapter 18 | */ 19 | 20 | @Component 21 | class MongoTeamRepository( 22 | private val mongo: CrudTeamRepository 23 | ) : TeamRepository { 24 | 25 | override fun existsByName(name: String): Boolean = 26 | mongo.existsById(name) 27 | 28 | override fun findByName(name: String): Team? { 29 | val dbTeam = mongo.findByName(name) 30 | return dbTeam?.let { DbTeam.toTeam(it) } 31 | } 32 | 33 | override fun findAll(): List = DbTeam.toTeams( 34 | mongo.findAll().toList()) 35 | 36 | override fun save(team: Team) { 37 | DbTeam.toDbTeam(team).also { mongo.save(it) } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/team/entities/DbEmployee.kt: -------------------------------------------------------------------------------- 1 | import pl.braintelligence.projectmanager.core.team.domain.Employee 2 | import pl.braintelligence.projectmanager.core.team.domain.JobPosition 3 | 4 | data class DbEmployee( 5 | private val firstName: String, 6 | private val lastName: String, 7 | private val jobPosition: String 8 | ) { 9 | companion object { 10 | fun toDbEmployee(employee: List): List = 11 | employee.map { toDbEmployee(it) } 12 | 13 | private fun toDbEmployee(employee: Employee): DbEmployee = 14 | DbEmployee( 15 | employee.firstName, 16 | employee.lastName, 17 | employee.jobPosition.toString() 18 | ) 19 | 20 | fun toEmployee(dbEmployee: DbEmployee): Employee = 21 | 22 | Employee( 23 | dbEmployee.firstName, 24 | dbEmployee.lastName, 25 | JobPosition.valueOf(dbEmployee.jobPosition) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/adapter/outgoing/mongo/team/entities/DbTeam.kt: -------------------------------------------------------------------------------- 1 | import org.springframework.data.annotation.Id 2 | import org.springframework.data.mongodb.core.mapping.Document 3 | import pl.braintelligence.projectmanager.core.team.domain.Team 4 | 5 | @Document(collection = "teams") 6 | data class DbTeam( 7 | @Id private val name: String, 8 | private val numberOfOngoingProjects: Int, 9 | private val members: List 10 | ) { 11 | companion object { 12 | fun toDbTeam(team: Team): DbTeam = 13 | DbTeam( 14 | team.name, 15 | team.numberOfOngoingProjects, 16 | DbEmployee.toDbEmployee(team.members) 17 | ) 18 | 19 | fun toTeams(dbTeams: List): List = 20 | dbTeams.map { 21 | Team( 22 | it.name, 23 | it.numberOfOngoingProjects, 24 | it.members.map { member -> DbEmployee.toEmployee(member) } 25 | ) 26 | } 27 | 28 | fun toTeam(dbTeam: DbTeam): Team? = 29 | Team( 30 | dbTeam.name, 31 | dbTeam.numberOfOngoingProjects, 32 | dbTeam.members.map { DbEmployee.toEmployee(it) } 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/infrastructure/error/ErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.infrastructure.error 2 | 3 | import org.slf4j.LoggerFactory 4 | import org.springframework.http.HttpStatus 5 | import org.springframework.http.ResponseEntity 6 | import org.springframework.http.ResponseEntity.status 7 | import org.springframework.web.bind.annotation.ExceptionHandler 8 | import org.springframework.web.bind.annotation.RestControllerAdvice 9 | import pl.braintelligence.projectmanager.core.team.domain.EntityAlreadyExistsException 10 | import pl.braintelligence.projectmanager.shared.DomainException 11 | import java.lang.invoke.MethodHandles 12 | import java.time.Instant 13 | import javax.servlet.http.HttpServletRequest 14 | 15 | 16 | @RestControllerAdvice 17 | class ErrorHandler { 18 | 19 | @ExceptionHandler(EntityAlreadyExistsException::class) 20 | fun handleEntityAlreadyExistsException(exception: DomainException, request: HttpServletRequest) 21 | : ResponseEntity { 22 | return mapToResponse(exception, request, HttpStatus.UNPROCESSABLE_ENTITY) 23 | } 24 | 25 | private fun mapToResponse(ex: DomainException, request: HttpServletRequest, httpStatus: HttpStatus): ResponseEntity { 26 | val errorMessage = ex.message?.let { ErrorMessage(it, Instant.now()) } 27 | log.error(createLog(request, httpStatus, ex.message)) 28 | return status(httpStatus) 29 | .body(errorMessage) 30 | } 31 | 32 | private fun createLog(request: HttpServletRequest, status: HttpStatus, message: String?): String { 33 | return "${request.method} on \"${request.requestURI}\" with status \"${status.value()}\" and message = \"$message\" " 34 | } 35 | 36 | companion object { 37 | private val log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) 38 | } 39 | } 40 | 41 | data class ErrorMessage(val message: String, val timestamp: Instant) 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/shared/DomainException.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.shared 2 | 3 | open class DomainException(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/pl/braintelligence/projectmanager/shared/InMemoryRepository.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.shared 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | abstract class InMemoryCrudRepository : CustomRepository { 6 | 7 | private val dataStore = ConcurrentHashMap() 8 | 9 | override fun save(entity: ENTITY, id: ID) = run { dataStore[id] = entity } 10 | 11 | override fun contains(id: ID): Boolean = dataStore.containsKey(id) 12 | 13 | override fun findById(id: ID): ENTITY? = dataStore[id] 14 | 15 | override fun findAll(): List = dataStore.values.toList() 16 | 17 | } 18 | 19 | interface CustomRepository { 20 | fun save(entity: ENTITY, id: ID) 21 | fun contains(id: ID): Boolean 22 | fun findById(id: ID): ENTITY? 23 | fun findAll(): List 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | client: 2 | connectTimeout: 1500 3 | readTimeout: 3000 4 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/base/BaseDtoObjects.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.base 2 | 3 | import pl.braintelligence.projectmanager.core.projects.domain.Feature 4 | import pl.braintelligence.projectmanager.core.projects.domain.PriorityLevel 5 | import pl.braintelligence.projectmanager.core.projects.domain.Status 6 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.NewTeam 7 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectDraft 8 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.ProjectWithFeatures 9 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 10 | 11 | trait BaseDtoObjects { 12 | 13 | // projects 14 | ProjectDraft newProjectDraftDto = new ProjectDraft("qwerty") 15 | 16 | Feature feature = new Feature("feature name 1", Status.TO_DO, PriorityLevel.NOT_DEFINED) 17 | 18 | ProjectWithFeatures newProjectWithFeaturesDto = new ProjectWithFeatures("feature 1", List.of(feature)) 19 | 20 | ProjectDraft newProjectDraft = new ProjectDraft("project name") 21 | 22 | // teams 23 | NewTeam newTeamDto = new NewTeam("123") 24 | NewTeam newTeamDto1 = new NewTeam("123456") 25 | 26 | TeamMember teamMemberDto = new TeamMember( 27 | "firstName", 28 | "LastName", 29 | "DEVELOPER" 30 | ) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/base/BaseIntegrationTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.base 2 | 3 | import com.github.tomakehurst.wiremock.junit.WireMockRule 4 | import org.junit.Rule 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.boot.test.web.client.TestRestTemplate 8 | import org.springframework.data.mongodb.core.MongoTemplate 9 | import org.springframework.http.ResponseEntity 10 | import pl.braintelligence.projectmanager.base.http.BaseHttpMethods 11 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.NewTeam 12 | import spock.lang.Specification 13 | 14 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT 15 | 16 | @SpringBootTest(webEnvironment = RANDOM_PORT) 17 | class BaseIntegrationTest extends Specification implements BaseHttpMethods, BaseDtoObjects { 18 | 19 | @Rule 20 | public WireMockRule reportingService = new WireMockRule(8081) 21 | 22 | @Autowired 23 | private TestRestTemplate restTemplate 24 | 25 | @Autowired 26 | private MongoTemplate mongoTemplate 27 | 28 | void setup() { 29 | clearMongoDB() 30 | } 31 | 32 | private void clearMongoDB() { 33 | for (def collection : mongoTemplate.collectionNames) { 34 | mongoTemplate.dropCollection(collection) 35 | } 36 | } 37 | 38 | protected ResponseEntity prepareNewTeam(String teamName) { 39 | def newTeam = new NewTeam(teamName) 40 | post("/teams", newTeam) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/base/BaseUnitTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.base 2 | 3 | 4 | import spock.lang.Specification 5 | 6 | class BaseUnitTest extends Specification implements BaseDtoObjects { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/base/http/BaseHttpMethods.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.base.http 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.boot.test.web.client.TestRestTemplate 5 | import org.springframework.core.ParameterizedTypeReference 6 | import org.springframework.http.HttpEntity 7 | import org.springframework.http.HttpHeaders 8 | import org.springframework.http.HttpMethod 9 | import org.springframework.http.ResponseEntity 10 | 11 | 12 | trait BaseHttpMethods { 13 | 14 | @Autowired 15 | TestRestTemplate restTemplate 16 | 17 | def ResponseEntity get(String uri, Class responseBodyType) { 18 | return sendRequest(uri, HttpMethod.GET, null, responseBodyType) 19 | } 20 | 21 | def ResponseEntity get(String uri, ParameterizedTypeReference responseBodyType) { 22 | return sendRequest(uri, HttpMethod.GET, null, responseBodyType) 23 | } 24 | 25 | ResponseEntity post(String uri, Object requestBody) { 26 | return sendRequest(uri, HttpMethod.POST, requestBody, Object) 27 | } 28 | 29 | def put(String uri, Object requestBody) { 30 | return sendRequest(uri, HttpMethod.PUT, requestBody, Object) 31 | } 32 | 33 | ResponseEntity patch(String uri) { 34 | return sendRequest(uri, HttpMethod.PATCH, null, Object) 35 | } 36 | 37 | ResponseEntity patch(String uri, Object requestBody) { 38 | return sendRequest(uri, HttpMethod.PATCH, requestBody, Object) 39 | } 40 | 41 | def HttpEntity preparePayload(T data, Map> additionalHeaders = [:]) { 42 | def headers = new HttpHeaders() 43 | 44 | headers.putAll(additionalHeaders) 45 | return new HttpEntity(data, headers) 46 | } 47 | 48 | private ResponseEntity sendRequest(String uri, HttpMethod method, Object requestBody, Class responseBodyType) { 49 | def entity = new HttpEntity<>(requestBody) 50 | return restTemplate.exchange(uri, method, entity, responseBodyType) 51 | } 52 | 53 | private ResponseEntity sendRequest(String uri, HttpMethod method, Object requestBody, ParameterizedTypeReference responseBodyType) { 54 | def entity = new HttpEntity<>(requestBody) 55 | return restTemplate.exchange(uri, method, entity, responseBodyType) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/project/ProjectAcceptanceTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.project 2 | 3 | 4 | import org.springframework.http.HttpStatus 5 | import pl.braintelligence.projectmanager.base.BaseIntegrationTest 6 | import pl.braintelligence.projectmanager.core.projects.domain.Project 7 | 8 | class ProjectAcceptanceTest extends BaseIntegrationTest { 9 | 10 | def "Should create project draft"() { 11 | given: "create new project draft" 12 | def response = post("/projects/drafts", newProjectDraft) 13 | response.statusCode == HttpStatus.CREATED 14 | 15 | when: "browse for this project draft" 16 | response = get("/projects/${response.body.id}", Project.class) 17 | 18 | then: 19 | response.statusCode == HttpStatus.OK 20 | 21 | when: "create new project with features" 22 | response = post("/projects", newProjectWithFeaturesDto) 23 | 24 | then: 25 | response.statusCode == HttpStatus.CREATED 26 | 27 | // when: "browse for all projects" 28 | // response = get("/projects", new ParameterizedTypeReference>() {}) 29 | 30 | // then: 31 | // response.statusCode == HttpStatus.OK 32 | 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/project/base/BaseProjectUnitTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.project.base 2 | 3 | import pl.braintelligence.projectmanager.base.BaseUnitTest 4 | import pl.braintelligence.projectmanager.core.projects.domain.Project 5 | import pl.braintelligence.projectmanager.core.projects.domain.ProjectFactory 6 | import pl.braintelligence.projectmanager.core.projects.domain.Status 7 | import pl.braintelligence.projectmanager.core.projects.domain.configuration.InMemoryProjectRepository 8 | import pl.braintelligence.projectmanager.core.projects.domain.configuration.ProjectConfiguration 9 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectCreatorPort 10 | import pl.braintelligence.projectmanager.core.projects.ports.incoming.ProjectQueryPort 11 | 12 | class BaseProjectUnitTest extends BaseUnitTest { 13 | 14 | protected InMemoryProjectRepository inMemoryProjectRepository = new InMemoryProjectRepository() 15 | 16 | protected ProjectCreatorPort projectCreator = 17 | new ProjectConfiguration() 18 | .buildProjectCreator(Mock(ProjectFactory), inMemoryProjectRepository) 19 | 20 | protected ProjectQueryPort projectQuery = 21 | new ProjectConfiguration() 22 | .buildProjectQuery(inMemoryProjectRepository) 23 | 24 | protected void verifyProjectDraft(Project project) { 25 | assert with(project) { 26 | id != null 27 | name == newProjectDraftDto.projectName 28 | status == Status.TO_DO 29 | teamAssigned == "" 30 | features == [] 31 | } 32 | } 33 | 34 | protected void verifyProjectWithFeatures(Project project) { 35 | assert with(project) { 36 | id != null 37 | name == newProjectWithFeaturesDto.projectName 38 | status == Status.TO_DO 39 | teamAssigned.isBlank() 40 | with(features[0]) { 41 | name == newProjectWithFeaturesDto.features[0].name 42 | status == newProjectWithFeaturesDto.features[0].status 43 | priorityLevel == newProjectWithFeaturesDto.features[0].priorityLevel 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/project/domain/ProjectCreationTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.project.domain 2 | 3 | 4 | import pl.braintelligence.projectmanager.project.base.BaseProjectUnitTest 5 | 6 | class ProjectCreationTest extends BaseProjectUnitTest { 7 | 8 | def "Should create a project draft and browse for it"() { 9 | when: 10 | def project = projectCreator.createProjectDraft(newProjectDraftDto) 11 | 12 | then: 13 | verifyProjectDraft(project) 14 | } 15 | 16 | def "Should create project with features and browse it"() { 17 | when: 18 | def project = projectCreator.createProjectWithFeatures(newProjectWithFeaturesDto) 19 | 20 | then: 21 | verifyProjectWithFeatures(project) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/project/domain/ProjectQueryTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.project.domain 2 | 3 | import pl.braintelligence.projectmanager.project.base.BaseProjectUnitTest 4 | 5 | class ProjectQueryTest extends BaseProjectUnitTest { 6 | 7 | def "Should browse for project"() { 8 | given: 9 | def project = projectCreator.createProjectWithFeatures(newProjectWithFeaturesDto) 10 | 11 | when: 12 | def response = projectQuery.getProject(project.id) 13 | 14 | then: 15 | verifyProjectWithFeatures(response) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/TeamAcceptanceTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team 2 | 3 | import org.springframework.core.ParameterizedTypeReference 4 | import org.springframework.http.HttpStatus 5 | import pl.braintelligence.projectmanager.base.BaseIntegrationTest 6 | import pl.braintelligence.projectmanager.core.team.domain.Team 7 | 8 | class TeamAcceptanceTest extends BaseIntegrationTest { 9 | 10 | def "User flow while using project manager"() { 11 | when: "new team is created" 12 | prepareNewTeam(newTeamDto.name) 13 | 14 | then: "user gets all teams created" 15 | get('/teams', new ParameterizedTypeReference>() {}) 16 | .statusCode == HttpStatus.OK 17 | 18 | when: "new member is added to a team" 19 | post('/teams/teamName/members', teamMemberDto) 20 | .statusCode == HttpStatus.CREATED 21 | 22 | then: "browse for new member" 23 | get('/teams', new ParameterizedTypeReference>() {}) 24 | .statusCode == HttpStatus.OK 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/base/BaseTeamUnitTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team.base 2 | 3 | import pl.braintelligence.projectmanager.base.BaseUnitTest 4 | import pl.braintelligence.projectmanager.core.team.domain.configuration.TeamConfiguration 5 | import pl.braintelligence.projectmanager.core.team.ports.incoming.TeamManager 6 | 7 | class BaseTeamUnitTest extends BaseUnitTest { 8 | 9 | protected TeamManager teamService = new TeamConfiguration().teamManager() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/domain/AddingTeamMembersToTeamTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team.domain 2 | 3 | 4 | import pl.braintelligence.projectmanager.team.base.BaseTeamUnitTest 5 | 6 | class AddingTeamMembersToTeamTest extends BaseTeamUnitTest { 7 | 8 | def "Should add member to a team"() { 9 | given: "new team is created" 10 | teamService.createTeam(newTeamDto) 11 | 12 | and: "member is added to a team" 13 | teamService.addMemberToTeam(newTeamDto.name, teamMemberDto) 14 | 15 | when: "teams are retrieved" 16 | def response = teamService.getTeam(newTeamDto.name) 17 | 18 | then: "new member is in a team" 19 | with(response.members[0]) { 20 | firstName == teamMemberDto.firstName 21 | lastName == teamMemberDto.lastName 22 | jobPosition.name() == teamMemberDto.jobPosition 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/domain/TeamCreationTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team.domain 2 | 3 | import pl.braintelligence.projectmanager.team.base.BaseTeamUnitTest 4 | 5 | class TeamCreationTest extends BaseTeamUnitTest { 6 | 7 | def "Should create a team (with default values)"() { 8 | given: 9 | teamService.createTeam(newTeamDto) 10 | 11 | when: 12 | def response = teamService.getTeam(newTeamDto.name) 13 | 14 | then: 15 | with(response) { 16 | name == newTeamDto.name 17 | numberOfOngoingProjects == 0 18 | members == [] 19 | } 20 | } 21 | 22 | def "Should create a teams (with default values)"() { 23 | given: 24 | teamService.createTeam(newTeamDto) 25 | teamService.createTeam(newTeamDto1) 26 | 27 | when: 28 | def response = teamService.getTeams() 29 | 30 | then: 31 | with(response) { 32 | name[0] == newTeamDto.name 33 | name[1] == newTeamDto1.name 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/domain/TeamMembersValidationTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team.domain 2 | 3 | 4 | import pl.braintelligence.projectmanager.core.team.domain.InvalidTeamMemberException 5 | import pl.braintelligence.projectmanager.infrastructure.adapter.incoming.rest.dto.TeamMember 6 | import pl.braintelligence.projectmanager.team.base.BaseTeamUnitTest 7 | import spock.lang.Unroll 8 | 9 | class TeamMembersValidationTest extends BaseTeamUnitTest { 10 | 11 | def "Should add member to a team"() { 12 | given: "team is created" 13 | def teamName = newTeamDto.name 14 | teamService.createTeam(newTeamDto) 15 | 16 | when: "two members are added to a team" 17 | teamService.addMemberToTeam(teamName, teamMemberDto) 18 | teamService.addMemberToTeam(teamName, teamMemberDto) 19 | 20 | then: "two members are in a team" 21 | teamService.getTeam(teamName).members.size() == 2 22 | } 23 | 24 | @Unroll 25 | def "Should throw IllegalArgumentException when #errorMessage"() { 26 | given: 27 | def teamMember = new TeamMember(firstName, lastName, jobPosition) 28 | teamService.createTeam(newTeamDto) 29 | 30 | when: 31 | teamService.addMemberToTeam(newTeamDto.name, teamMember) 32 | 33 | then: 34 | def thrown = thrown(InvalidTeamMemberException.class) 35 | thrown.message == errorMessage 36 | 37 | where: 38 | errorMessage | firstName | lastName | jobPosition 39 | "Empty member first name." | ' ' | 'valid last name' | 'DEVELOPER' 40 | "Empty member last name." | 'valid first name' | ' ' | 'DEVELOPER' 41 | "Invalid job position." | 'valid first name' | 'valid last name' | 'upps! not valid' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/groovy/pl/braintelligence/projectmanager/team/domain/TeamValidationTest.groovy: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.team.domain 2 | 3 | 4 | import pl.braintelligence.projectmanager.core.team.domain.EntityAlreadyExistsException 5 | import pl.braintelligence.projectmanager.core.team.domain.InvalidTeamException 6 | import pl.braintelligence.projectmanager.core.team.domain.MissingTeamException 7 | import pl.braintelligence.projectmanager.core.team.domain.Team 8 | import pl.braintelligence.projectmanager.team.base.BaseTeamUnitTest 9 | 10 | class TeamValidationTest extends BaseTeamUnitTest { 11 | 12 | def "Should throw exception when team does not exist"() { 13 | when: 14 | teamService.addMemberToTeam("non-existent team name", teamMemberDto) 15 | 16 | then: 17 | def thrown = thrown(MissingTeamException.class) 18 | thrown.message == "Team does not exist." 19 | } 20 | 21 | def "Should throw an exception when team name is empty"() { 22 | when: 23 | new Team(teamName) 24 | 25 | then: 26 | def ex = thrown(InvalidTeamException.class) 27 | ex.message == "Empty team name." 28 | 29 | where: 30 | teamName << ['', ' '] 31 | } 32 | 33 | def "Should not create team that already exists"() { 34 | given: "create a team" 35 | teamService.createTeam(newTeamDto) 36 | 37 | when: "create another team with the same name" 38 | teamService.createTeam(newTeamDto) 39 | 40 | then: 41 | def thrown = thrown(EntityAlreadyExistsException.class) 42 | thrown.message == "Team already exist." 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/kotlin/pl/braintelligence/projectmanager/core/team/HexagonalArchitectureTest.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team 2 | 3 | import com.tngtech.archunit.junit.AnalyzeClasses 4 | import com.tngtech.archunit.junit.ArchTest 5 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes 6 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 7 | import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition 8 | import pl.braintelligence.projectmanager.Application 9 | 10 | /** 11 | * Feel free to create PR with more rules :) 12 | */ 13 | 14 | //@TestInstance(TestInstance.Lifecycle.PER_CLASS) 15 | @AnalyzeClasses(packagesOf = [Application::class]) 16 | internal class HexagonalArchitectureTest { 17 | 18 | @ArchTest 19 | val `other domain features does dont depend on each other` = 20 | SlicesRuleDefinition.slices() 21 | .matching("..core.(*)..") 22 | .should() 23 | .notDependOnEachOther() 24 | 25 | @ArchTest 26 | val `Core (domain) does not have infrastructure code` = 27 | noClasses() 28 | .that() 29 | .resideInAPackage("..core..") 30 | .should() 31 | .accessClassesThat() 32 | .resideInAnyPackage("..infrastructure..") 33 | 34 | @ArchTest 35 | val `Controllers are in adapters` = 36 | classes() 37 | .that() 38 | .haveSimpleNameStartingWith("Controller") 39 | .should() 40 | .resideInAPackage("..adapter..") 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/kotlin/pl/braintelligence/projectmanager/core/team/NoSpringInDomainTest.kt: -------------------------------------------------------------------------------- 1 | package pl.braintelligence.projectmanager.core.team 2 | 3 | import com.tngtech.archunit.junit.AnalyzeClasses 4 | import com.tngtech.archunit.junit.ArchTest 5 | import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses 6 | import org.junit.jupiter.api.TestInstance 7 | 8 | /** 9 | * Feel free to create PR with more rules :) 10 | */ 11 | 12 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 13 | @AnalyzeClasses(packagesOf = [NoSpringInDomainTest::class]) 14 | internal class NoSpringInDomainTest { 15 | 16 | @ArchTest 17 | val `Core (domain) should not depend on spring` = 18 | noClasses() 19 | .that() 20 | .resideInAPackage("..pl.braintelligence.projectmanager.core..domain..") 21 | .should() 22 | .dependOnClassesThat() 23 | .resideInAPackage("org.springframework..") 24 | } 25 | --------------------------------------------------------------------------------