From 1d4b5694dd131f1e822e3a11a21a83fd13a47d24 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Wed, 1 Mar 2017 18:26:42 +0700 Subject: [PATCH 01/17] Kotlin! --- .gitignore | 45 +-- .travis.yml | 2 +- README.md | 27 +- build.gradle | 114 +++++--- db/.gitkeep | 0 gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 54208 -> 53556 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- scripts/deploy.sh | 2 +- src/main/java/com/myapp/Application.java | 20 -- src/main/java/com/myapp/Utils.java | 21 -- .../java/com/myapp/auth/SecurityConfig.java | 92 ------- .../auth/StatelessAuthenticationFilter.java | 39 --- .../auth/TokenAuthenticationService.java | 11 - .../auth/TokenAuthenticationServiceImpl.java | 33 --- .../java/com/myapp/auth/TokenHandler.java | 16 -- .../java/com/myapp/auth/TokenHandlerImpl.java | 59 ---- .../com/myapp/auth/UserAuthentication.java | 52 ---- .../myapp/config/PooledDatasourceConfig.java | 22 -- .../java/com/myapp/config/QueryDSLConfig.java | 21 -- .../java/com/myapp/config/Swagger2Config.java | 23 -- .../com/myapp/controller/AuthController.java | 64 ----- .../com/myapp/controller/FeedController.java | 34 --- .../myapp/controller/MicropostController.java | 37 --- .../controller/RelationshipController.java | 35 --- .../com/myapp/controller/UserController.java | 76 ----- .../controller/UserMicropostController.java | 35 --- .../UserRelationshipController.java | 35 --- src/main/java/com/myapp/domain/Micropost.java | 45 --- .../java/com/myapp/domain/Relationship.java | 35 --- src/main/java/com/myapp/domain/User.java | 103 ------- src/main/java/com/myapp/domain/UserStats.java | 22 -- .../java/com/myapp/dto/ErrorResponse.java | 9 - .../java/com/myapp/dto/MicropostParams.java | 14 - src/main/java/com/myapp/dto/PageParams.java | 27 -- src/main/java/com/myapp/dto/PostDTO.java | 48 ---- .../java/com/myapp/dto/RelatedUserDTO.java | 22 -- src/main/java/com/myapp/dto/UserDTO.java | 21 -- src/main/java/com/myapp/dto/UserParams.java | 55 ---- .../repository/MicropostCustomRepository.java | 26 -- .../MicropostCustomRepositoryImpl.java | 79 ------ .../myapp/repository/MicropostRepository.java | 7 - .../RelatedUserCustomRepository.java | 26 -- .../RelatedUserCustomRepositoryImpl.java | 80 ------ .../repository/RelationshipRepository.java | 18 -- .../repository/UserCustomRepository.java | 21 -- .../repository/UserCustomRepositoryImpl.java | 43 --- .../com/myapp/repository/UserRepository.java | 15 - .../helper/UserStatsQueryHelper.java | 42 --- .../com/myapp/service/MicropostService.java | 23 -- .../myapp/service/MicropostServiceImpl.java | 115 -------- .../myapp/service/RelationshipService.java | 19 -- .../service/RelationshipServiceImpl.java | 102 ------- .../myapp/service/SecurityContextService.java | 9 - .../service/SecurityContextServiceImpl.java | 29 -- .../java/com/myapp/service/UserService.java | 29 -- .../com/myapp/service/UserServiceImpl.java | 112 -------- .../exceptions/NotPermittedException.java | 9 - .../RelationshipNotFoundException.java | 4 - .../exceptions/UserNotFoundException.java | 4 - src/main/kotlin/com/myapp/Application.kt | 13 + src/main/kotlin/com/myapp/Utils.kt | 10 + .../kotlin/com/myapp/auth/SecurityConfig.kt | 77 ++++++ .../com/myapp/auth/SecurityContextService.kt | 7 + .../myapp/auth/SecurityContextServiceImpl.kt | 30 ++ .../auth/StatelessAuthenticationFilter.kt | 34 +++ .../myapp/auth/TokenAuthenticationService.kt | 11 + .../auth/TokenAuthenticationServiceImpl.kt | 24 ++ .../kotlin/com/myapp/auth/TokenHandler.kt | 13 + .../kotlin/com/myapp/auth/TokenHandlerImpl.kt | 43 +++ .../com/myapp/auth/UserAuthentication.kt | 23 ++ .../kotlin/com/myapp/config/Swagger2Config.kt | 25 ++ .../com/myapp/controller/AuthController.kt | 44 +++ .../com/myapp/controller/FeedController.kt | 30 ++ .../myapp/controller/MicropostController.kt | 34 +++ .../myapp/controller/RelatedUserController.kt | 36 +++ .../controller/RelationshipController.kt | 35 +++ .../com/myapp/controller/UserController.kt | 59 ++++ .../controller/UserMicropostController.kt | 35 +++ .../kotlin/com/myapp/domain/HasIdentity.kt | 13 + src/main/kotlin/com/myapp/domain/Micropost.kt | 37 +++ .../kotlin/com/myapp/domain/RelatedUser.kt | 29 ++ .../kotlin/com/myapp/domain/Relationship.kt | 7 + src/main/kotlin/com/myapp/domain/User.kt | 55 ++++ .../com/myapp/domain/UserDetailsImpl.kt | 16 ++ src/main/kotlin/com/myapp/domain/UserStats.kt | 15 + src/main/kotlin/com/myapp/dto/page/Page.kt | 6 + .../kotlin/com/myapp/dto/page/PageImpl.kt | 6 + .../com/myapp/dto/request/PageParams.kt | 7 + .../com/myapp/dto/request/UserEditParams.kt | 13 + .../com/myapp/dto/request/UserNewParams.kt | 11 + .../com/myapp/dto/response/ErrorResponse.kt | 6 + .../com/myapp/repository/FeedRepository.kt | 8 + .../myapp/repository/FeedRepositoryImpl.kt | 36 +++ .../myapp/repository/MicropostRepository.kt | 12 + .../repository/MicropostRepositoryImpl.kt | 65 +++++ src/main/kotlin/com/myapp/repository/Pager.kt | 17 ++ .../myapp/repository/RelatedUserRepository.kt | 9 + .../repository/RelatedUserRepositoryImpl.kt | 60 ++++ .../repository/RelationshipRepository.kt | 12 + .../repository/RelationshipRepositoryImpl.kt | 81 ++++++ .../com/myapp/repository/UserRepository.kt | 13 + .../myapp/repository/UserRepositoryImpl.kt | 111 ++++++++ .../exception/EmailDuplicatedException.kt | 5 + .../exception/RecordInvalidException.kt | 5 + .../exception/RecordNotFoundException.kt | 3 + .../RelationshipDuplicatedException.kt | 5 + .../kotlin/com/myapp/service/FeedService.kt | 10 + .../com/myapp/service/FeedServiceImpl.kt | 24 ++ .../com/myapp/service/MicropostService.kt | 12 + .../com/myapp/service/MicropostServiceImpl.kt | 48 ++++ .../com/myapp/service/RelatedUserService.kt | 9 + .../myapp/service/RelatedUserServiceImpl.kt | 48 ++++ .../com/myapp/service/RelationshipService.kt | 6 + .../myapp/service/RelationshipServiceImpl.kt | 44 +++ .../myapp/service/UserDetailsServiceImpl.kt | 25 ++ .../kotlin/com/myapp/service/UserService.kt | 14 + .../com/myapp/service/UserServiceImpl.kt | 62 +++++ .../com/myapp/service/WithCurrentUser.kt | 16 ++ .../exception/NotAuthorizedException.kt | 3 + .../RelationshipNotFoundException.kt | 3 + src/main/resources/application.yml | 52 +--- .../db/migration/V1__create-schema.sql | 37 --- src/main/resources/log4jdbc.log4j2.properties | 2 - src/main/resources/logback.xml | 6 +- .../com/myapp/auth/TestSecurityConfig.groovy | 29 -- .../controller/AuthControllerTest.groovy | 93 ------- .../controller/BaseControllerTest.groovy | 60 ---- .../controller/FeedControllerTest.groovy | 66 ----- .../controller/MicropostControllerTest.groovy | 97 ------- .../RelationshipControllerTest.groovy | 83 ------ .../controller/UserControllerTest.groovy | 259 ------------------ .../UserMicropostControllerTest.groovy | 92 ------- .../UserRelationshipControllerTest.groovy | 95 ------- .../repository/BaseRepositoryTest.groovy | 13 - .../MicropostCustomRepositoryTest.groovy | 87 ------ .../RelatedUserCustomRepositoryTest.groovy | 55 ---- .../RelationshipRepositoryTest.groovy | 53 ---- .../repository/RepositoryTestConfig.groovy | 14 - .../UserCustomRepositoryTest.groovy | 28 -- .../com/myapp/service/BaseServiceTest.groovy | 27 -- .../myapp/service/MicropostServiceTest.groovy | 149 ---------- .../service/RelationshipServiceTest.groovy | 114 -------- .../com/myapp/service/UserServiceTest.groovy | 152 ---------- .../myapp/auth/SecurityContextServiceTest.kt | 56 ++++ .../auth/TokenAuthenticationServiceTest.kt | 44 +++ .../kotlin/com/myapp/auth/TokenHandlerTest.kt | 55 ++++ .../myapp/controller/AuthControllerTest.kt | 76 +++++ .../myapp/controller/BaseControllerTest.kt | 58 ++++ .../myapp/controller/FeedControllerTest.kt | 53 ++++ .../controller/MicropostControllerTest.kt | 86 ++++++ .../controller/RelatedUserControllerTest.kt | 73 +++++ .../controller/RelationshipControllerTest.kt | 78 ++++++ .../myapp/controller/UserControllerTest.kt | 254 +++++++++++++++++ .../controller/UserMicropostControllerTest.kt | 87 ++++++ .../myapp/repository/BaseRepositoryTest.kt | 45 +++ .../myapp/repository/FeedRepositoryTest.kt | 71 +++++ .../repository/MicropostRepositoryTest.kt | 125 +++++++++ .../repository/RelatedUserRepositoryTest.kt | 128 +++++++++ .../repository/RelationshipRepositoryTest.kt | 145 ++++++++++ .../myapp/repository/UserRepositoryTest.kt | 203 ++++++++++++++ .../com/myapp/service/FeedServiceTest.kt | 55 ++++ .../com/myapp/service/MicropostServiceTest.kt | 144 ++++++++++ .../myapp/service/RelatedUserServiceTest.kt | 105 +++++++ .../myapp/service/RelationshipServiceTest.kt | 70 +++++ .../myapp/service/UserDetailsServiceTest.kt | 40 +++ .../com/myapp/service/UserServiceTest.kt | 176 ++++++++++++ src/test/kotlin/com/myapp/testing/TestUtil.kt | 24 ++ 168 files changed, 3958 insertions(+), 3657 deletions(-) delete mode 100644 db/.gitkeep create mode 100644 gradle.properties delete mode 100644 src/main/java/com/myapp/Application.java delete mode 100644 src/main/java/com/myapp/Utils.java delete mode 100644 src/main/java/com/myapp/auth/SecurityConfig.java delete mode 100644 src/main/java/com/myapp/auth/StatelessAuthenticationFilter.java delete mode 100644 src/main/java/com/myapp/auth/TokenAuthenticationService.java delete mode 100644 src/main/java/com/myapp/auth/TokenAuthenticationServiceImpl.java delete mode 100644 src/main/java/com/myapp/auth/TokenHandler.java delete mode 100644 src/main/java/com/myapp/auth/TokenHandlerImpl.java delete mode 100644 src/main/java/com/myapp/auth/UserAuthentication.java delete mode 100644 src/main/java/com/myapp/config/PooledDatasourceConfig.java delete mode 100644 src/main/java/com/myapp/config/QueryDSLConfig.java delete mode 100644 src/main/java/com/myapp/config/Swagger2Config.java delete mode 100644 src/main/java/com/myapp/controller/AuthController.java delete mode 100644 src/main/java/com/myapp/controller/FeedController.java delete mode 100644 src/main/java/com/myapp/controller/MicropostController.java delete mode 100644 src/main/java/com/myapp/controller/RelationshipController.java delete mode 100644 src/main/java/com/myapp/controller/UserController.java delete mode 100644 src/main/java/com/myapp/controller/UserMicropostController.java delete mode 100644 src/main/java/com/myapp/controller/UserRelationshipController.java delete mode 100644 src/main/java/com/myapp/domain/Micropost.java delete mode 100644 src/main/java/com/myapp/domain/Relationship.java delete mode 100644 src/main/java/com/myapp/domain/User.java delete mode 100644 src/main/java/com/myapp/domain/UserStats.java delete mode 100644 src/main/java/com/myapp/dto/ErrorResponse.java delete mode 100644 src/main/java/com/myapp/dto/MicropostParams.java delete mode 100644 src/main/java/com/myapp/dto/PageParams.java delete mode 100644 src/main/java/com/myapp/dto/PostDTO.java delete mode 100644 src/main/java/com/myapp/dto/RelatedUserDTO.java delete mode 100644 src/main/java/com/myapp/dto/UserDTO.java delete mode 100644 src/main/java/com/myapp/dto/UserParams.java delete mode 100644 src/main/java/com/myapp/repository/MicropostCustomRepository.java delete mode 100644 src/main/java/com/myapp/repository/MicropostCustomRepositoryImpl.java delete mode 100644 src/main/java/com/myapp/repository/MicropostRepository.java delete mode 100644 src/main/java/com/myapp/repository/RelatedUserCustomRepository.java delete mode 100644 src/main/java/com/myapp/repository/RelatedUserCustomRepositoryImpl.java delete mode 100644 src/main/java/com/myapp/repository/RelationshipRepository.java delete mode 100644 src/main/java/com/myapp/repository/UserCustomRepository.java delete mode 100644 src/main/java/com/myapp/repository/UserCustomRepositoryImpl.java delete mode 100644 src/main/java/com/myapp/repository/UserRepository.java delete mode 100644 src/main/java/com/myapp/repository/helper/UserStatsQueryHelper.java delete mode 100644 src/main/java/com/myapp/service/MicropostService.java delete mode 100644 src/main/java/com/myapp/service/MicropostServiceImpl.java delete mode 100644 src/main/java/com/myapp/service/RelationshipService.java delete mode 100644 src/main/java/com/myapp/service/RelationshipServiceImpl.java delete mode 100644 src/main/java/com/myapp/service/SecurityContextService.java delete mode 100644 src/main/java/com/myapp/service/SecurityContextServiceImpl.java delete mode 100644 src/main/java/com/myapp/service/UserService.java delete mode 100644 src/main/java/com/myapp/service/UserServiceImpl.java delete mode 100644 src/main/java/com/myapp/service/exceptions/NotPermittedException.java delete mode 100644 src/main/java/com/myapp/service/exceptions/RelationshipNotFoundException.java delete mode 100644 src/main/java/com/myapp/service/exceptions/UserNotFoundException.java create mode 100644 src/main/kotlin/com/myapp/Application.kt create mode 100644 src/main/kotlin/com/myapp/Utils.kt create mode 100644 src/main/kotlin/com/myapp/auth/SecurityConfig.kt create mode 100644 src/main/kotlin/com/myapp/auth/SecurityContextService.kt create mode 100644 src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/auth/StatelessAuthenticationFilter.kt create mode 100644 src/main/kotlin/com/myapp/auth/TokenAuthenticationService.kt create mode 100644 src/main/kotlin/com/myapp/auth/TokenAuthenticationServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/auth/TokenHandler.kt create mode 100644 src/main/kotlin/com/myapp/auth/TokenHandlerImpl.kt create mode 100644 src/main/kotlin/com/myapp/auth/UserAuthentication.kt create mode 100644 src/main/kotlin/com/myapp/config/Swagger2Config.kt create mode 100644 src/main/kotlin/com/myapp/controller/AuthController.kt create mode 100644 src/main/kotlin/com/myapp/controller/FeedController.kt create mode 100644 src/main/kotlin/com/myapp/controller/MicropostController.kt create mode 100644 src/main/kotlin/com/myapp/controller/RelatedUserController.kt create mode 100644 src/main/kotlin/com/myapp/controller/RelationshipController.kt create mode 100644 src/main/kotlin/com/myapp/controller/UserController.kt create mode 100644 src/main/kotlin/com/myapp/controller/UserMicropostController.kt create mode 100644 src/main/kotlin/com/myapp/domain/HasIdentity.kt create mode 100644 src/main/kotlin/com/myapp/domain/Micropost.kt create mode 100644 src/main/kotlin/com/myapp/domain/RelatedUser.kt create mode 100644 src/main/kotlin/com/myapp/domain/Relationship.kt create mode 100644 src/main/kotlin/com/myapp/domain/User.kt create mode 100644 src/main/kotlin/com/myapp/domain/UserDetailsImpl.kt create mode 100644 src/main/kotlin/com/myapp/domain/UserStats.kt create mode 100644 src/main/kotlin/com/myapp/dto/page/Page.kt create mode 100644 src/main/kotlin/com/myapp/dto/page/PageImpl.kt create mode 100644 src/main/kotlin/com/myapp/dto/request/PageParams.kt create mode 100644 src/main/kotlin/com/myapp/dto/request/UserEditParams.kt create mode 100644 src/main/kotlin/com/myapp/dto/request/UserNewParams.kt create mode 100644 src/main/kotlin/com/myapp/dto/response/ErrorResponse.kt create mode 100644 src/main/kotlin/com/myapp/repository/FeedRepository.kt create mode 100644 src/main/kotlin/com/myapp/repository/FeedRepositoryImpl.kt create mode 100644 src/main/kotlin/com/myapp/repository/MicropostRepository.kt create mode 100644 src/main/kotlin/com/myapp/repository/MicropostRepositoryImpl.kt create mode 100644 src/main/kotlin/com/myapp/repository/Pager.kt create mode 100644 src/main/kotlin/com/myapp/repository/RelatedUserRepository.kt create mode 100644 src/main/kotlin/com/myapp/repository/RelatedUserRepositoryImpl.kt create mode 100644 src/main/kotlin/com/myapp/repository/RelationshipRepository.kt create mode 100644 src/main/kotlin/com/myapp/repository/RelationshipRepositoryImpl.kt create mode 100644 src/main/kotlin/com/myapp/repository/UserRepository.kt create mode 100644 src/main/kotlin/com/myapp/repository/UserRepositoryImpl.kt create mode 100644 src/main/kotlin/com/myapp/repository/exception/EmailDuplicatedException.kt create mode 100644 src/main/kotlin/com/myapp/repository/exception/RecordInvalidException.kt create mode 100644 src/main/kotlin/com/myapp/repository/exception/RecordNotFoundException.kt create mode 100644 src/main/kotlin/com/myapp/repository/exception/RelationshipDuplicatedException.kt create mode 100644 src/main/kotlin/com/myapp/service/FeedService.kt create mode 100644 src/main/kotlin/com/myapp/service/FeedServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/MicropostService.kt create mode 100644 src/main/kotlin/com/myapp/service/MicropostServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/RelatedUserService.kt create mode 100644 src/main/kotlin/com/myapp/service/RelatedUserServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/RelationshipService.kt create mode 100644 src/main/kotlin/com/myapp/service/RelationshipServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/UserDetailsServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/UserService.kt create mode 100644 src/main/kotlin/com/myapp/service/UserServiceImpl.kt create mode 100644 src/main/kotlin/com/myapp/service/WithCurrentUser.kt create mode 100644 src/main/kotlin/com/myapp/service/exception/NotAuthorizedException.kt create mode 100644 src/main/kotlin/com/myapp/service/exception/RelationshipNotFoundException.kt delete mode 100644 src/main/resources/db/migration/V1__create-schema.sql delete mode 100644 src/main/resources/log4jdbc.log4j2.properties delete mode 100644 src/test/groovy/com/myapp/auth/TestSecurityConfig.groovy delete mode 100644 src/test/groovy/com/myapp/controller/AuthControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/BaseControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/FeedControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/MicropostControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/RelationshipControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/UserControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/UserMicropostControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/controller/UserRelationshipControllerTest.groovy delete mode 100644 src/test/groovy/com/myapp/repository/BaseRepositoryTest.groovy delete mode 100644 src/test/groovy/com/myapp/repository/MicropostCustomRepositoryTest.groovy delete mode 100644 src/test/groovy/com/myapp/repository/RelatedUserCustomRepositoryTest.groovy delete mode 100644 src/test/groovy/com/myapp/repository/RelationshipRepositoryTest.groovy delete mode 100644 src/test/groovy/com/myapp/repository/RepositoryTestConfig.groovy delete mode 100644 src/test/groovy/com/myapp/repository/UserCustomRepositoryTest.groovy delete mode 100644 src/test/groovy/com/myapp/service/BaseServiceTest.groovy delete mode 100644 src/test/groovy/com/myapp/service/MicropostServiceTest.groovy delete mode 100644 src/test/groovy/com/myapp/service/RelationshipServiceTest.groovy delete mode 100644 src/test/groovy/com/myapp/service/UserServiceTest.groovy create mode 100644 src/test/kotlin/com/myapp/auth/SecurityContextServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/auth/TokenAuthenticationServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/auth/TokenHandlerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/AuthControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/BaseControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/FeedControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/MicropostControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/RelatedUserControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/RelationshipControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/UserControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/controller/UserMicropostControllerTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/BaseRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/FeedRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/MicropostRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/RelatedUserRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/RelationshipRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/repository/UserRepositoryTest.kt create mode 100644 src/test/kotlin/com/myapp/service/FeedServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/service/MicropostServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/service/RelatedUserServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/service/RelationshipServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/service/UserDetailsServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/service/UserServiceTest.kt create mode 100644 src/test/kotlin/com/myapp/testing/TestUtil.kt diff --git a/.gitignore b/.gitignore index a125cfa..eb82cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,28 @@ -target - -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm -*.iml - -## Directory-based project format: -.idea/ - -# jrebel configuration file -rebel.xml - -# H2 db files -db/**/*.db +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar +generated/ +db/ +.envrc -# Elastic Beanstalk Files -.elasticbeanstalk/* -!.elasticbeanstalk/*.cfg.yml -!.elasticbeanstalk/*.global.yml +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans -.deploy +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr -.envrc -generated/ +### NetBeans ### +nbproject/private/ build/ -.gradle/ \ No newline at end of file +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ diff --git a/.travis.yml b/.travis.yml index af9137e..90d7f34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - pip install awscli - aws --version -script: ./gradlew clean build jacocoTestReport coveralls +script: ./gradlew clean jooqGenerate build jacocoTestReport coveralls before_deploy: # Parse branch name and determine an environment to deploy diff --git a/README.md b/README.md index 1e61790..da22d75 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,18 @@ This repository is an example application for Spring Boot and Angular2 tutorial. [Demo](https://micropost.hana053.com/) +* [Spring Boot](https://projects.spring.io/spring-boot/) +* [Kotlin](https://kotlinlang.org/) +* [jOOQ](https://www.jooq.org/) +* [Flyway](https://flywaydb.org/) * JWT -* [Querydsl](http://www.querydsl.com/) -* [Spock](http://spockframework.org/) ## Getting Started Run Spring Boot. ``` -./gradlew bootRun +./gradlew jooqGenerate bootRun ``` Serve frontend app. @@ -29,6 +31,7 @@ git clone https://github.com/springboot-angular2-tutorial/angular2-app.git Testing. ``` +./gradlew jooqGenerate # If you have not generated jOOQ code yet. ./gradlew test ``` @@ -39,22 +42,22 @@ API documentation. open http://localhost:8080/swagger-ui.html ``` +After you migrated DB. +``` +./gradlew jooqGenerate # It will generate jOOQ code for your new schema. +``` + ## Frequently asked questions -* Q) Build becomes an error on IntelliJ IDEA with error message "QUser, QRelationship and etc can't be found". -* A) You must configure setting for Annotation Processors. - 1. Go to Preferences -> Build, Execution, Deployment -> Annotation Processors - 2. Check Enable annotation processing checkbox - 3. In "Store generated sources relative to:" select Module content root. - 4. Finally, Build -> Build Project - +* Q) IntelliJ IDEA is very slow when you use jOOQ with Kotlin. +* A) Refer [this ticket](https://youtrack.jetbrains.com/issue/KT-10978). In my case, [tuning memory config](https://youtrack.jetbrains.com/issue/KT-10978#comment=27-1519260) of IntelliJ IDEA worked. ## Docker Support Dev ```bash -./gradlew clean build -x test +./gradlew clean jooqGenerate build -x test docker build -t YOUR_IMAGE_NAME . docker run -p 8080:8080 YOUR_IMAGE_NAME ``` @@ -62,7 +65,7 @@ docker run -p 8080:8080 YOUR_IMAGE_NAME Prod ```bash -./gradlew clean build -x test +./gradlew clean jooqGenerate build -x test docker build --build-arg JASYPT_ENCRYPTOR_PASSWORD=secret -t YOUR_IMAGE_NAME . docker run -p 8080:8080 \ -e "SPRING_PROFILES_ACTIVE=prod" \ diff --git a/build.gradle b/build.gradle index e7fba86..1cab3c8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,29 @@ +import groovy.xml.MarkupBuilder +import org.jooq.util.GenerationTool + +import javax.xml.bind.JAXB + buildscript { ext { + kotlinVersion = '1.0.6' springBootVersion = '1.5.1.RELEASE' - querydslVersion = '4.1.4' + jooqVersion = '3.9.0' + flywayVersion = '4.1.1' + h2Version = '1.4.193' swaggerVersion = '2.6.1' - spockVersion = '1.1-groovy-2.4-rc-3' } repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } + maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' } } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - classpath "org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.1" + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + classpath "org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}" + classpath "org.jooq:jooq-codegen:${jooqVersion}" + classpath "com.h2database:h2:${h2Version}" + classpath "gradle.plugin.com.boxfuse.client:flyway-release:${flywayVersion}" } } @@ -19,55 +31,48 @@ plugins { id 'jacoco' id 'com.github.kt3k.coveralls' version '2.8.1' } - apply plugin: 'java' -apply plugin: 'groovy' -apply plugin: 'idea' +apply plugin: 'kotlin' +apply plugin: 'kotlin-spring' apply plugin: 'org.springframework.boot' -apply plugin: "com.github.kt3k.coveralls" - -sourceCompatibility = 1.8 +// It's just needed for JOOQ codegen +apply plugin: "org.flywaydb.flyway" jar { baseName = 'springboot-angular2-tutorial' version = '0.0.1' } -idea { - module { - sourceDirs += file('generated/') - generatedSourceDirs += file('generated/') - } -} +sourceCompatibility = 1.8 repositories { jcenter() + maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' } } + dependencies { - compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion - compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: springBootVersion - compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: springBootVersion - compile group: 'org.springframework.boot', name: 'spring-boot-actuator', version: springBootVersion - compile group: 'org.springframework.boot', name: 'spring-boot-configuration-processor', version: springBootVersion - compile group: 'org.springframework.boot', name: 'spring-boot-devtools', version: springBootVersion + compile group: 'org.springframework.boot', name: 'spring-boot-starter-web' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-security' + compile group: 'org.springframework.boot', name: 'spring-boot-starter-jooq' + compile group: 'org.springframework.boot', name: 'spring-boot-actuator' + compile group: 'org.springframework.boot', name: 'spring-boot-devtools' + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: kotlinVersion + compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlinVersion + compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.8.6' + compile group: 'org.jooq', name: 'jooq', version: jooqVersion + compile group: 'org.flywaydb', name: 'flyway-core', version: flywayVersion + compile group: 'com.zaxxer', name: 'HikariCP' compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0' - compile group: 'org.bgee.log4jdbc-log4j2', name: 'log4jdbc-log4j2-jdbc4.1', version: '1.16' - compile group: 'org.flywaydb', name: 'flyway-core', version: '3.2.1' - compile group: 'com.querydsl', name: 'querydsl-jpa', version: querydslVersion - compile group: 'com.querydsl', name: 'querydsl-sql-spring', version: querydslVersion - compile "com.querydsl:querydsl-apt:$querydslVersion:jpa" - compile group: 'com.github.ulisesbocchio', name: 'jasypt-spring-boot-starter', version: '1.11' - compile group: 'com.zaxxer', name: 'HikariCP', version: '2.5.1' compile group: 'io.springfox', name: 'springfox-swagger2', version: swaggerVersion compile group: 'io.springfox', name: 'springfox-swagger-ui', version: swaggerVersion - compileOnly "org.projectlombok:lombok:1.16.14" + compile group: 'com.github.ulisesbocchio', name: 'jasypt-spring-boot-starter', version: '1.11' runtime group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '1.5.8' - runtime group: 'com.h2database', name: 'h2', version: '1.4.193' - testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion - testCompile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.8' - testCompile group: 'org.spockframework', name: 'spock-core', version: spockVersion - testCompile group: 'org.spockframework', name: 'spock-spring', version: spockVersion - testCompile group: 'com.jayway.jsonpath', name: 'json-path', version: '2.2.0' + runtime group: 'com.h2database', name: 'h2', version: h2Version + testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' + testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit', version: kotlinVersion + testCompile group: 'org.assertj', name: 'assertj-core', version: '3.6.2' + testCompile group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.1.0' + testCompile group: 'com.beust', name: 'klaxon', version: '0.27' } jacocoTestReport { @@ -77,3 +82,40 @@ jacocoTestReport { } } +flyway { + url = 'jdbc:h2:./db/dev;MODE=MySQL' + user = 'sa' + password = '' +} + +task jooqGenerate { + doLast { + def writer = new StringWriter() + new MarkupBuilder(writer) + .configuration('xmlns': "http://www.jooq.org/xsd/jooq-codegen-${jooqVersion}.xsd") { + jdbc { + driver 'org.h2.Driver' + url 'jdbc:h2:./db/dev;MODE=MySQL' + user 'sa' + password '' + } + generator { + database { + inputSchema 'public' + outputSchemaToDefault 'true' + } + generate { + } + target { + packageName 'com.myapp.generated' + directory 'src/main/java' + } + } + } + GenerationTool.generate( + JAXB.unmarshal(new StringReader(writer.toString()), org.jooq.util.jaxb.Configuration.class) + ) + } +} +jooqGenerate.dependsOn flywayMigrate + diff --git a/db/.gitkeep b/db/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..587b727 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.incremental=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index af6d66231069ff0a583f5c7273c8fcea424e61c2..ca78035ef0501d802d4fc55381ef2d5c3ce0ec6e 100644 GIT binary patch delta 23126 zcmZ5{b8x1?vuX%r|vz!bEfK@db?)2|9Wcr znSOeHR)Dt_gCi-)fR11O!Aq>4ys6IWFF#%e@OI z$bWm-{`2^^3te#nH*!#m(B>_1~YDx3IgLmAQkPwW+b2qYI;{ zow2KHnX0`bnlMTL79e75;jqf6E^TwwN9{P;alr@=HZBU5MZQ{IPRQOE+_K)&%rWVq z;VH(R{T}+QB(~*88VM-+?a=HM|Kl`2U(f5&>Dn#Gq?Zj@TreyU9unM^W}CAqJ`RIZ zRcrNz5M8&^mJDg^XM2z@!UVR#ewBWBrz*dh)be1tkyf>Dkk^s`57q;N&e@bSD#Gd|2}<+HBLHpKTN~MY z%3gZ+DQqjHS5yt?$#ex7FrJh12(N*Yt}Ak_+GPO9G0nt=LcmvO`VIpAb|6XR6R0xT z7X+8;XH%8!Duzbq+c5@AOPetx57yg7YUUFuo`BI>(_pb`mEyUYvCy}tU8M5<8NVeX zMdWW(3YPV}gYMk7F&q1_F(}HvNJxb5HQ4w895NsO(%=Dt*3RaFh7Fa0TLU(NEnqLsV){L&!H zS*~`$&_WpKJp5F*NUl?yC!217fz>{T@*$^ypskYcOPMU;z4tzLju{7(dj*hv9ue0*!;rMBPUu;dJzy1!;X z?cEcB@R}2wzsvriC$m!lYh#(|e_}R;3vT~EAlTBQp9l6I7$i8<-x`4g0r~R}mTdop zK_@gapsW+RIl*81Y%1$+d(t1IN_HS&txCZFtfk=ebrUO{Lc}si%#I08{qCK(9C=xP z62XfP_q(&a5>|2sE$C5PNy)Jg-lL%V!4X`hd>kgC;k}NW^nOC5ccQh-!F4zu1;yCvu$b+f)!E`b zz#0-^8JWu2obG&fyA?cDH{qu?n{W%_bR{BrD*QhbUg#F-(Ht~0N(ldUdIxn*YV9Gng`@4Yr7n@na;Cc^lteC+!6Q)bH|MY1fZ zxi+Ykiq0@-o5c?kh%%8V?Zw}L?%mPwf zkDM8t1k@A<3kizlu4E`g%W%vgrRU(IAmi9bxvIwl7B+a}$oW*TndQI8B(R_da~#d1 zDzUXZ7f_eUoO2u3+QaBSWyepp>4kHgm{RNXGyg_?+949@5gOa_?zm+B5ndIY0dC>p zhx#wn93zY{>MY;d$d6@MJm2}RjJPlUR3F?_HaAg{KeG@P&|X&4Eg*UGT_p(Dm2J;S z;Fb$A8RCa=m9nlQW20j--eHx%(Q43cWi4Y}ZP<4A4Qq}CJg^cCD&Y1sss{55$>1{m zOm1|(HzTC6@zH2A!!Vixn8aQ;0X64h$8oSj0gU6BKVt%VDUQs0$&8m|#54ArM$Pqx zYXbC=@9_*Wl76ZN^pYOQ%QYL`yx-8^nfRSHlNPaF$GZ*_M8gm`IzN%6SDS^e*qP}q z*qOv3WNDbycah)ZircZVioH5l_g#wwj3s!Q_eQ*az5N)C82`9WicLte1mfS7e@M4jG0mWHlSPpW$hYiKVoV_k%&Wuqx1iw5 z4O&;wqXnjM^%>XvTPihu@Xr#EGx|GIT^n9c(uRzFrdQX<1WBwiX8A+?=12O}l6(W7 zL>cwC0@)MW<%c1opvpT<#PN_6F}dV&vj1%#$ZEcAi&sI);m`%A2sGVMPt|u$_2jcL zsNd>zkc33kT)HDtA*;vBZp3y{7>xU!8%mBm$8uGIvEy~|R+Irc!3KW&^M)pM?k5*B zqEJE*jaq21UbzwvVpmVpHjP?d`k?`hK>*^)j35mW40OLFG*esuDAPffn&2103|l3x z6I^Rh|gI07z7R!7KUaX3m(l0ok`psm$&Hk5jLokNk>#rqH7hf}}MmKWu`= z4lc}_s(7ZwsC|3emCzmS5YxDL`vQl8YK-b&#}%dijf_aofXa)_fJd?Mfr{;&r`$Ov zt2=Bj&!0G8knr@oqa}kUYar66z9;h);JW@(j!UH(7CQ4j1jt|Z##KlB@$Ptmijc$` zJeDZQwl^Ho8LA<7h$7Iopb2JJLbz85zIO-+___477G-F4faTpW@e$H1H1QGB%lG?B zG{4dX8LZVWB>$x)7@9@YFd=zVW>zD=0{n@dr@~^=EAow#h`KJ8b$~P;YqS@60Bwmh z^@us`H_tmYz~(Onje*KVl*ZEzYCFRKOc|EI)Xq`snP;UCmfP31|u z4MQ9_we&S*UqUYXR@)wIX*%o5_7-mCR;eT|EepwcC;toa|I7%}hz1f#|7gcM6bJ~z ze;xTK!_Y0UMwMXynJ9b+S0Vn>j%AAa&%`n7av6;Y4FckT1Oh?@l2SAb0H$fYo8oJt zey^@#dZ%zlyRNr|m{7;jZ(vEQ0;JLJR>bNW{?ICHcG}vKJJQ~E_tXrByPyMJ{UN|o zKcKL6Xog!s1AZrf^>66ww$m=XK=*OoTJKJCvF+t_{QaK$;sVVb9XRBU9wf>gQ6YeG z7^dkX5mgR92FPu+n>C@00Arkol@kedDX5_|PgDq$w^OfmS!*Slu*QV_DB=?)ftL3w z2*fdmN6vm=1+fRJ&VG0WNh6lx-Gn2jX5W4UV|UF6hLMLc%6>HAOk)dS4a0Zp%HP=t zhS)iKDCapWezGTyV|RHt{rF?AcmBVesl$nf??idLsiS+~kYcrBfI80OPI_BffUaIh1q4L7yS!};Mz-(8f=6dB=C<1Ger(}s{)2!Ec2w_e zX2jRx;8C^L+Ti&@U|PlIH|o&xn9LM>w?G{OhWkC!z9q+2qI)B*O9n#m4MEeD$f{>r zpSdj^U3XsI{B~J@M4Oy^PCQbnM>m$^2Yh!A8}Vb6Pka8orG)PeAk|LZZO(I`ZVla0 zy7RZ4a}Oq@`^>U?B}scB&!0@HsMurcRqPzQDHELbf#^wMpdRERrm6TZuZy}BUu@^q zD!*1UY$n@TG|H3t45Ip@vYUgxsqP~yNdIxbS$s|5kfzJiNV0vW?Z(12P*te6@dSYjE>Vv%4y>fxG+C{UQNt-xXjwJlS+Mc=Chi5+O0C zJ0~w|`WmenSn4{;=#b>4nlq}Y1>qeAhvHMDZ><8j}7&W~~?pfIHIbQ1e!G z_K6^5DbY+DKw1O;Ru*j-^<>*yyg%Jz*}48Ek1?PQWC^O!?oLs7p^#nE6KCmn5;b%y zdCQC5J3{V#D-lhQxR=BP4i>1_9;ssje;H!7dP%0AGx8Z>xRlpmLp<9SJApR9A2tVv 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

>RR&Za{kvKkS^xkc}eI58<gs}XQt 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5db2ff7..4f2492d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Wed Feb 22 11:26:30 ICT 2017 +#Thu Feb 23 11:57:23 ICT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 71ddebd..5394d17 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -11,7 +11,7 @@ readonly AWS_ACCOUNT_NUMBER=$(aws sts get-caller-identity --output text --query readonly IMAGE_URL=${AWS_ACCOUNT_NUMBER}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${DOCKER_NAME} # Build -./gradlew build -x test +./gradlew jooqGenerate build -x test # Ensure docker repository exists aws ecr describe-repositories --repository-names ${DOCKER_NAME} > /dev/null 2>&1 || \ diff --git a/src/main/java/com/myapp/Application.java b/src/main/java/com/myapp/Application.java deleted file mode 100644 index b5aae2b..0000000 --- a/src/main/java/com/myapp/Application.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.myapp; - -import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@EnableEncryptableProperties -public class Application { - - @SuppressWarnings("unused") - private static final Logger logger = LoggerFactory.getLogger(Application.class); - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - diff --git a/src/main/java/com/myapp/Utils.java b/src/main/java/com/myapp/Utils.java deleted file mode 100644 index 0b9be56..0000000 --- a/src/main/java/com/myapp/Utils.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.myapp; - -import javax.xml.bind.DatatypeConverter; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class Utils { - - public static String md5(String source) { - byte[] bytes; - try { - bytes = MessageDigest.getInstance("MD5") - .digest(source.getBytes("UTF-8")); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - return null; - } - return DatatypeConverter.printHexBinary(bytes).toLowerCase(); - } - -} diff --git a/src/main/java/com/myapp/auth/SecurityConfig.java b/src/main/java/com/myapp/auth/SecurityConfig.java deleted file mode 100644 index 8f51f0e..0000000 --- a/src/main/java/com/myapp/auth/SecurityConfig.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.myapp.auth; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -@Order(1) -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - private final UserDetailsService userService; - private final StatelessAuthenticationFilter statelessAuthenticationFilter; - - @Autowired - public SecurityConfig(UserDetailsService userService, StatelessAuthenticationFilter statelessAuthenticationFilter) { - super(true); - this.userService = userService; - this.statelessAuthenticationFilter = statelessAuthenticationFilter; - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - // we use jwt so that we can disable csrf protection - http.csrf().disable(); - - http - .exceptionHandling().and() - .anonymous().and() - .servletApi().and() - .headers().cacheControl() - ; - - http.authorizeRequests() - .antMatchers(HttpMethod.GET, "/api/users").hasRole("USER") - .antMatchers(HttpMethod.GET, "/api/users/me").hasRole("USER") - .antMatchers(HttpMethod.PATCH, "/api/users/me").hasRole("USER") - .antMatchers(HttpMethod.GET, "/api/users/me/microposts").hasRole("USER") - .antMatchers(HttpMethod.POST, "/api/microposts/**").hasRole("USER") - .antMatchers(HttpMethod.DELETE, "/api/microposts/**").hasRole("USER") - .antMatchers(HttpMethod.POST, "/api/relationships/**").hasRole("USER") - .antMatchers(HttpMethod.DELETE, "/api/relationships/**").hasRole("USER") - .antMatchers(HttpMethod.GET, "/api/feed").hasRole("USER") - .and() - .exceptionHandling() - .authenticationEntryPoint(new Http401AuthenticationEntryPoint("'Bearer token_type=\"JWT\"'")); - - http.addFilterBefore(statelessAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - /** - * Prevent StatelessAuthenticationFilter will be added to Spring Boot filter chain. - * Only Spring Security must use it. - */ - @Bean - public FilterRegistrationBean registration(StatelessAuthenticationFilter filter) { - FilterRegistrationBean registration = new FilterRegistrationBean(filter); - registration.setEnabled(false); - return registration; - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder()); - } - - @Override - protected UserDetailsService userDetailsService() { - return userService; - } - -} - - diff --git a/src/main/java/com/myapp/auth/StatelessAuthenticationFilter.java b/src/main/java/com/myapp/auth/StatelessAuthenticationFilter.java deleted file mode 100644 index e2b5823..0000000 --- a/src/main/java/com/myapp/auth/StatelessAuthenticationFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.myapp.auth; - -import io.jsonwebtoken.JwtException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.GenericFilterBean; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -@Component -class StatelessAuthenticationFilter extends GenericFilterBean { - - private final TokenAuthenticationService tokenAuthenticationService; - - StatelessAuthenticationFilter(TokenAuthenticationService tokenAuthenticationService) { - this.tokenAuthenticationService = tokenAuthenticationService; - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - try { - Authentication authentication = tokenAuthenticationService.getAuthentication((HttpServletRequest) req); - SecurityContextHolder.getContext().setAuthentication(authentication); - chain.doFilter(req, res); - SecurityContextHolder.getContext().setAuthentication(null); - } catch (AuthenticationException | JwtException e) { - SecurityContextHolder.clearContext(); - ((HttpServletResponse) res).setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } - } -} diff --git a/src/main/java/com/myapp/auth/TokenAuthenticationService.java b/src/main/java/com/myapp/auth/TokenAuthenticationService.java deleted file mode 100644 index ec2efce..0000000 --- a/src/main/java/com/myapp/auth/TokenAuthenticationService.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.myapp.auth; - -import org.springframework.security.core.Authentication; - -import javax.servlet.http.HttpServletRequest; - -public interface TokenAuthenticationService { - - Authentication getAuthentication(HttpServletRequest request); -} - diff --git a/src/main/java/com/myapp/auth/TokenAuthenticationServiceImpl.java b/src/main/java/com/myapp/auth/TokenAuthenticationServiceImpl.java deleted file mode 100644 index d3842c5..0000000 --- a/src/main/java/com/myapp/auth/TokenAuthenticationServiceImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.myapp.auth; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Service; - -import javax.servlet.http.HttpServletRequest; - -@Service -class TokenAuthenticationServiceImpl implements TokenAuthenticationService { - - private final TokenHandler tokenHandler; - - @Autowired - TokenAuthenticationServiceImpl(TokenHandler tokenHandler) { - this.tokenHandler = tokenHandler; - } - - public Authentication getAuthentication(HttpServletRequest request) { - final String authHeader = request.getHeader("authorization"); - if (authHeader == null) return null; - if (!authHeader.startsWith("Bearer")) return null; - - final String jwt = authHeader.substring(7); - if (jwt.isEmpty()) return null; - - return tokenHandler - .parseUserFromToken(jwt) - .map(UserAuthentication::new) - .orElse(null); - } -} - diff --git a/src/main/java/com/myapp/auth/TokenHandler.java b/src/main/java/com/myapp/auth/TokenHandler.java deleted file mode 100644 index 5c9a123..0000000 --- a/src/main/java/com/myapp/auth/TokenHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.myapp.auth; - -import com.myapp.domain.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -public interface TokenHandler { - - Optional parseUserFromToken(String token); - - String createTokenForUser(User user); - -} diff --git a/src/main/java/com/myapp/auth/TokenHandlerImpl.java b/src/main/java/com/myapp/auth/TokenHandlerImpl.java deleted file mode 100644 index de39fad..0000000 --- a/src/main/java/com/myapp/auth/TokenHandlerImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.myapp.auth; - -import com.myapp.domain.User; -import com.myapp.repository.UserRepository; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import java.time.ZonedDateTime; -import java.util.Date; -import java.util.Optional; - -@Component -public final class TokenHandlerImpl implements TokenHandler { - - @SuppressWarnings("unused") - private static final Logger logger = LoggerFactory.getLogger(TokenHandlerImpl.class); - - private final String secret; - - private final UserRepository userRepository; - - @Autowired - public TokenHandlerImpl(@Value("${app.jwt.secret}") String secret, - UserRepository userRepository) { - this.secret = secret; - this.userRepository = userRepository; - } - - @Override - public Optional parseUserFromToken(String token) { - final String subject = Jwts.parser() - .setSigningKey(secret) - .parseClaimsJws(token) - .getBody() - .getSubject(); - final Long userId = Long.valueOf(subject); - final User user = userRepository.findOne(userId); - - return Optional.ofNullable(user); - } - - @Override - public String createTokenForUser(User user) { - final ZonedDateTime afterOneWeek = ZonedDateTime.now().plusWeeks(1); - - return Jwts.builder() - .setSubject(user.getId().toString()) - .signWith(SignatureAlgorithm.HS512, secret) - .setExpiration(Date.from(afterOneWeek.toInstant())) - .compact(); - } -} - diff --git a/src/main/java/com/myapp/auth/UserAuthentication.java b/src/main/java/com/myapp/auth/UserAuthentication.java deleted file mode 100644 index 1499e16..0000000 --- a/src/main/java/com/myapp/auth/UserAuthentication.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.myapp.auth; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; - -public class UserAuthentication implements Authentication { - - private final UserDetails user; - private boolean authenticated = true; - - public UserAuthentication(UserDetails user) { - this.user = user; - } - - @Override - public Collection getAuthorities() { - return user.getAuthorities(); - } - - @Override - public Object getCredentials() { - return user.getPassword(); - } - - @Override - public UserDetails getDetails() { - return user; - } - - @Override - public Object getPrincipal() { - return user.getUsername(); - } - - @Override - public boolean isAuthenticated() { - return authenticated; - } - - @Override - public void setAuthenticated(boolean authenticated) throws IllegalArgumentException { - this.authenticated = authenticated; - } - - @Override - public String getName() { - return user.getUsername(); - } -} diff --git a/src/main/java/com/myapp/config/PooledDatasourceConfig.java b/src/main/java/com/myapp/config/PooledDatasourceConfig.java deleted file mode 100644 index c13d6d2..0000000 --- a/src/main/java/com/myapp/config/PooledDatasourceConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.myapp.config; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; - -import javax.sql.DataSource; -import java.sql.SQLException; - -@Configuration -@Profile({"dev2", "stg", "prod"}) -@ConfigurationProperties(prefix = "spring.datasource") -public class PooledDatasourceConfig extends HikariConfig { - - @Bean - public DataSource dataSource() throws SQLException { - return new HikariDataSource(this); - } -} diff --git a/src/main/java/com/myapp/config/QueryDSLConfig.java b/src/main/java/com/myapp/config/QueryDSLConfig.java deleted file mode 100644 index 7c03394..0000000 --- a/src/main/java/com/myapp/config/QueryDSLConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.myapp.config; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -@Configuration -public class QueryDSLConfig { - - @PersistenceContext - private EntityManager entityManager; - - @Bean - public JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(entityManager); - } - -} diff --git a/src/main/java/com/myapp/config/Swagger2Config.java b/src/main/java/com/myapp/config/Swagger2Config.java deleted file mode 100644 index adc1b89..0000000 --- a/src/main/java/com/myapp/config/Swagger2Config.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.myapp.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; -import springfox.documentation.swagger2.annotations.EnableSwagger2; - -import static springfox.documentation.builders.PathSelectors.regex; - -@Configuration -@EnableSwagger2 -public class Swagger2Config { - - @Bean - public Docket swaggerSpringMvcPlugin() { - return new Docket(DocumentationType.SWAGGER_2) - .select() - .paths(regex("/api.*")) - .build(); - } - -} diff --git a/src/main/java/com/myapp/controller/AuthController.java b/src/main/java/com/myapp/controller/AuthController.java deleted file mode 100644 index 9eab04c..0000000 --- a/src/main/java/com/myapp/controller/AuthController.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.myapp.controller; - -import com.myapp.auth.TokenHandler; -import com.myapp.service.SecurityContextService; -import lombok.Value; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author riccardo.causo - */ -@RestController -@RequestMapping("/api/auth") -public class AuthController { - - private final AuthenticationManager authenticationManager; - private final TokenHandler tokenHandler; - private final SecurityContextService securityContextService; - - @Autowired - AuthController(AuthenticationManager authenticationManager, - TokenHandler tokenHandler, - SecurityContextService securityContextService) { - this.authenticationManager = authenticationManager; - this.tokenHandler = tokenHandler; - this.securityContextService = securityContextService; - } - - @RequestMapping(method = RequestMethod.POST) - public AuthResponse auth(@RequestBody AuthParams params) throws AuthenticationException { - final UsernamePasswordAuthenticationToken loginToken = params.toAuthenticationToken(); - final Authentication authentication = authenticationManager.authenticate(loginToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - - return securityContextService.currentUser().map(u -> { - final String token = tokenHandler.createTokenForUser(u); - return new AuthResponse(token); - }).orElseThrow(RuntimeException::new); // it does not happen. - } - - @Value - private static final class AuthParams { - private final String email; - private final String password; - - UsernamePasswordAuthenticationToken toAuthenticationToken() { - return new UsernamePasswordAuthenticationToken(email, password); - } - } - - @Value - private static final class AuthResponse { - private final String token; - } - -} diff --git a/src/main/java/com/myapp/controller/FeedController.java b/src/main/java/com/myapp/controller/FeedController.java deleted file mode 100644 index 1a80f8a..0000000 --- a/src/main/java/com/myapp/controller/FeedController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.myapp.controller; - -import com.myapp.dto.PageParams; -import com.myapp.dto.PostDTO; -import com.myapp.service.MicropostService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api/feed") -public class FeedController { - - @SuppressWarnings("UnusedDeclaration") - private static final Logger logger = LoggerFactory.getLogger(FeedController.class); - - private final MicropostService micropostService; - - @Autowired - public FeedController(MicropostService micropostService) { - this.micropostService = micropostService; - } - - @RequestMapping(method = RequestMethod.GET) - public List feed(PageParams pageParams) { - return micropostService.findAsFeed(pageParams); - } - -} diff --git a/src/main/java/com/myapp/controller/MicropostController.java b/src/main/java/com/myapp/controller/MicropostController.java deleted file mode 100644 index 7ab8db8..0000000 --- a/src/main/java/com/myapp/controller/MicropostController.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.myapp.controller; - -import com.myapp.domain.Micropost; -import com.myapp.dto.MicropostParams; -import com.myapp.service.MicropostService; -import com.myapp.service.exceptions.NotPermittedException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/microposts") -public class MicropostController { - - private final MicropostService micropostService; - - @Autowired - public MicropostController(MicropostService micropostService) { - this.micropostService = micropostService; - } - - @RequestMapping(method = RequestMethod.POST) - public Micropost create(@RequestBody MicropostParams params) { - return micropostService.saveMyPost(params.toPost()); - } - - @RequestMapping(value = "{id}", method = RequestMethod.DELETE) - public void delete(@PathVariable("id") Long id) throws NotPermittedException { - micropostService.delete(id); - } - - @ResponseStatus(value = HttpStatus.FORBIDDEN) - @ExceptionHandler(NotPermittedException.class) - public void handleNoPermission() { - } - -} diff --git a/src/main/java/com/myapp/controller/RelationshipController.java b/src/main/java/com/myapp/controller/RelationshipController.java deleted file mode 100644 index 9955581..0000000 --- a/src/main/java/com/myapp/controller/RelationshipController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.myapp.controller; - -import com.myapp.service.RelationshipService; -import com.myapp.service.exceptions.RelationshipNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/relationships") -public class RelationshipController { - - private final RelationshipService relationshipService; - - @Autowired - public RelationshipController(RelationshipService relationshipService) { - this.relationshipService = relationshipService; - } - - @RequestMapping(value = "/to/{followedId}", method = RequestMethod.POST) - public void follow(@PathVariable("followedId") Long followedId) { - relationshipService.follow(followedId); - } - - @RequestMapping(value = "/to/{followedId}", method = RequestMethod.DELETE) - public void unfollow(@PathVariable("followedId") Long followedId) throws RelationshipNotFoundException { - relationshipService.unfollow(followedId); - } - - @ResponseStatus(value = HttpStatus.NOT_FOUND) - @ExceptionHandler(RelationshipNotFoundException.class) - public void handleRelationshipNotFound() { - } - -} diff --git a/src/main/java/com/myapp/controller/UserController.java b/src/main/java/com/myapp/controller/UserController.java deleted file mode 100644 index 85a4339..0000000 --- a/src/main/java/com/myapp/controller/UserController.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.myapp.controller; - -import com.myapp.domain.User; -import com.myapp.dto.ErrorResponse; -import com.myapp.dto.UserDTO; -import com.myapp.dto.UserParams; -import com.myapp.service.UserService; -import com.myapp.service.exceptions.UserNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.Nullable; -import javax.validation.Valid; -import java.util.Optional; - -@RestController -@RequestMapping("/api/users") -public class UserController { - - private static final Integer DEFAULT_PAGE_SIZE = 5; - - private final UserService userService; - - @Autowired - public UserController(UserService userService) { - this.userService = userService; - } - - @RequestMapping(method = RequestMethod.GET) - public Page list(@RequestParam(value = "page", required = false) @Nullable Integer page, - @RequestParam(value = "size", required = false) @Nullable Integer size) { - final PageRequest pageable = new PageRequest( - Optional.ofNullable(page).orElse(1) - 1, - Optional.ofNullable(size).orElse(DEFAULT_PAGE_SIZE)); - return userService.findAll(pageable); - } - - @RequestMapping(method = RequestMethod.POST) - public User create(@Valid @RequestBody UserParams params) { - return userService.create(params); - } - - @RequestMapping(method = RequestMethod.GET, path = "{id:\\d+}") - public UserDTO show(@PathVariable("id") Long id) throws UserNotFoundException { - return userService.findOne(id) - .orElseThrow(UserNotFoundException::new); - } - - @RequestMapping(method = RequestMethod.GET, path = "/me") - public UserDTO showMe() { - return userService.findMe() - .orElseThrow(() -> new AccessDeniedException("")); - } - - @RequestMapping(method = RequestMethod.PATCH, path = "/me") - public void updateMe(@Valid @RequestBody UserParams params) { - userService.updateMe(params); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(DataIntegrityViolationException.class) - public ErrorResponse handleValidationException(DataIntegrityViolationException e) { - return new ErrorResponse("email_already_taken", "This email is already taken."); - } - - @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No user") - @ExceptionHandler(UserNotFoundException.class) - public void handleUserNotFound() { - } - -} diff --git a/src/main/java/com/myapp/controller/UserMicropostController.java b/src/main/java/com/myapp/controller/UserMicropostController.java deleted file mode 100644 index e0075f0..0000000 --- a/src/main/java/com/myapp/controller/UserMicropostController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.myapp.controller; - -import com.myapp.dto.PageParams; -import com.myapp.dto.PostDTO; -import com.myapp.service.MicropostService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api/users") -public class UserMicropostController { - - private final MicropostService micropostService; - - @Autowired - public UserMicropostController(MicropostService micropostService) { - this.micropostService = micropostService; - } - - @RequestMapping(method = RequestMethod.GET, path = "/{userId:\\d+}/microposts") - public List list(@PathVariable("userId") Long userId, PageParams pageParams) { - return micropostService.findByUser(userId, pageParams); - } - - @RequestMapping(method = RequestMethod.GET, path = "/me/microposts") - public List list(PageParams pageParams) { - return micropostService.findMyPosts(pageParams); - } - -} diff --git a/src/main/java/com/myapp/controller/UserRelationshipController.java b/src/main/java/com/myapp/controller/UserRelationshipController.java deleted file mode 100644 index 88aedc1..0000000 --- a/src/main/java/com/myapp/controller/UserRelationshipController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.myapp.controller; - -import com.myapp.dto.PageParams; -import com.myapp.dto.RelatedUserDTO; -import com.myapp.service.RelationshipService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api/users/{userId}") -public class UserRelationshipController { - - private final RelationshipService relationshipService; - - @Autowired - public UserRelationshipController(RelationshipService relationshipService) { - this.relationshipService = relationshipService; - } - - @RequestMapping(method = RequestMethod.GET, path = "/followings") - public List followings(@PathVariable("userId") long userId, PageParams pageParams) { - return relationshipService.findFollowings(userId, pageParams); - } - - @RequestMapping(method = RequestMethod.GET, path = "/followers") - public List followers(@PathVariable("userId") long userId, PageParams pageParams) { - return relationshipService.findFollowers(userId, pageParams); - } - -} diff --git a/src/main/java/com/myapp/domain/Micropost.java b/src/main/java/com/myapp/domain/Micropost.java deleted file mode 100644 index e39930d..0000000 --- a/src/main/java/com/myapp/domain/Micropost.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.myapp.domain; - -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.util.Date; - -@Entity -@Table(name = "micropost") -@Data -@NoArgsConstructor -public class Micropost { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @NotNull - @ManyToOne(fetch = FetchType.EAGER) - private User user; - - @NotNull - private String content; - - @NotNull - @Column(name = "created_at") - private Date createdAt; - - public Micropost(User user, String content) { - this.user = user; - this.content = content; - } - - public Micropost(String content) { - this.content = content; - } - - @PrePersist - protected void onCreate() { - createdAt = new Date(); - } - -} diff --git a/src/main/java/com/myapp/domain/Relationship.java b/src/main/java/com/myapp/domain/Relationship.java deleted file mode 100644 index 59e31e1..0000000 --- a/src/main/java/com/myapp/domain/Relationship.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.myapp.domain; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Data; -import lombok.NoArgsConstructor; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import java.io.Serializable; - -@Entity -@Table(name = "relationship", uniqueConstraints = @UniqueConstraint(columnNames = {"follower_id", "followed_id"})) -@NoArgsConstructor -@Data -public class Relationship implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private Long id; - - @NotNull - @ManyToOne - @JsonIgnore - private User follower; - - @NotNull - @ManyToOne - @JsonIgnore - private User followed; - - public Relationship(User follower, User followed) { - this.follower = follower; - this.followed = followed; - } -} diff --git a/src/main/java/com/myapp/domain/User.java b/src/main/java/com/myapp/domain/User.java deleted file mode 100644 index 0b4e65b..0000000 --- a/src/main/java/com/myapp/domain/User.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.myapp.domain; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -@Entity -@Table(name = "user", uniqueConstraints = @UniqueConstraint(columnNames = {"username"})) -@ToString -public class User implements UserDetails { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Getter - @Setter - private Long id; - - @NotNull - @Size(min = 4, max = 30) - @Setter - @Pattern(regexp = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$") - private String username; - - @NotNull - private String password; - - @NotNull - @Size(min = 4, max = 30) - @Getter - @Setter - private String name; - - @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, orphanRemoval = true) - @JsonIgnore - private List microposts; - - @OneToMany(mappedBy = "follower", fetch = FetchType.LAZY, orphanRemoval = true) - @JsonIgnore - private List followerRelations; - - @OneToMany(mappedBy = "followed", fetch = FetchType.LAZY, orphanRemoval = true) - @JsonIgnore - private List followedRelations; - - @Override - @JsonProperty("email") - public String getUsername() { - return username; - } - - @Override - @JsonIgnore - public String getPassword() { - return password; - } - - @JsonProperty - public void setPassword(String password) { - this.password = password; - } - - @Override - @JsonIgnore - public Collection getAuthorities() { - return Collections.singleton(() -> "ROLE_USER"); - } - - @Override - @JsonIgnore - public boolean isAccountNonExpired() { - return true; - } - - @Override - @JsonIgnore - public boolean isAccountNonLocked() { - return true; - } - - @Override - @JsonIgnore - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - @JsonIgnore - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/com/myapp/domain/UserStats.java b/src/main/java/com/myapp/domain/UserStats.java deleted file mode 100644 index 8cc2ac4..0000000 --- a/src/main/java/com/myapp/domain/UserStats.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.myapp.domain; - -import com.querydsl.core.annotations.QueryProjection; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder -public class UserStats { - - private final long micropostCnt; - private final long followingCnt; - private final long followerCnt; - - @QueryProjection - public UserStats(long micropostCnt, long followingCnt, long followerCnt) { - this.micropostCnt = micropostCnt; - this.followingCnt = followingCnt; - this.followerCnt = followerCnt; - } - -} diff --git a/src/main/java/com/myapp/dto/ErrorResponse.java b/src/main/java/com/myapp/dto/ErrorResponse.java deleted file mode 100644 index 6f00cbc..0000000 --- a/src/main/java/com/myapp/dto/ErrorResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.myapp.dto; - -import lombok.Value; - -@Value -public class ErrorResponse { - private final String code; - private final String message; -} diff --git a/src/main/java/com/myapp/dto/MicropostParams.java b/src/main/java/com/myapp/dto/MicropostParams.java deleted file mode 100644 index 7119393..0000000 --- a/src/main/java/com/myapp/dto/MicropostParams.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.myapp.dto; - -import com.myapp.domain.Micropost; -import lombok.Value; - -@Value -public class MicropostParams { - - private String content; - - public Micropost toPost() { - return new Micropost(content); - } -} diff --git a/src/main/java/com/myapp/dto/PageParams.java b/src/main/java/com/myapp/dto/PageParams.java deleted file mode 100644 index aa7ac7c..0000000 --- a/src/main/java/com/myapp/dto/PageParams.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.myapp.dto; - -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Optional; - -@Data -@NoArgsConstructor -public final class PageParams { - - private static final int DEFAULT_COUNT = 20; - - private Long sinceId; - private Long maxId; - private int count = DEFAULT_COUNT; - - public Optional getSinceId() { - return Optional.ofNullable(sinceId); - } - - public Optional getMaxId() { - return Optional.ofNullable(maxId); - } - - -} diff --git a/src/main/java/com/myapp/dto/PostDTO.java b/src/main/java/com/myapp/dto/PostDTO.java deleted file mode 100644 index dad1fe2..0000000 --- a/src/main/java/com/myapp/dto/PostDTO.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.myapp.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.myapp.Utils; -import com.myapp.domain.Micropost; -import com.myapp.domain.UserStats; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -import java.util.Date; - -@Value -@Builder -public class PostDTO { - - private final long id; - @NonNull - private final String content; - @NonNull - private final Date createdAt; - @JsonProperty("user") - private final UserDTO userDTO; - private final Boolean isMyPost; - - public static PostDTO newInstance(Micropost post, UserStats userStats, Boolean isMyPost, Boolean isFollowedByMe) { - final UserDTO userDTO = UserDTO.builder() - .id(post.getUser().getId()) - .name(post.getUser().getName()) - .userStats(userStats) - .avatarHash(Utils.md5(post.getUser().getUsername())) - .isFollowedByMe(isFollowedByMe) - .build(); - - return PostDTO.builder() - .id(post.getId()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .userDTO(userDTO) - .isMyPost(isMyPost) - .build(); - } - - public static PostDTO newInstance(Micropost post, Boolean isMyPost) { - return PostDTO.newInstance(post, null, isMyPost, null); - } - -} diff --git a/src/main/java/com/myapp/dto/RelatedUserDTO.java b/src/main/java/com/myapp/dto/RelatedUserDTO.java deleted file mode 100644 index de5bd67..0000000 --- a/src/main/java/com/myapp/dto/RelatedUserDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.myapp.dto; - -import com.myapp.domain.UserStats; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Value -@Builder -public class RelatedUserDTO { - - private final long id; - @NonNull - private final String name; - @NonNull - private final String avatarHash; - @NonNull - private final UserStats userStats; - private final long relationshipId; - private final Boolean isFollowedByMe; - -} diff --git a/src/main/java/com/myapp/dto/UserDTO.java b/src/main/java/com/myapp/dto/UserDTO.java deleted file mode 100644 index 7ee2ca2..0000000 --- a/src/main/java/com/myapp/dto/UserDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.myapp.dto; - -import com.myapp.domain.UserStats; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Value -@Builder -public class UserDTO { - - private final long id; - private final String email; - @NonNull - private final String name; - @NonNull - private final String avatarHash; - private final UserStats userStats; - private final Boolean isFollowedByMe; - -} diff --git a/src/main/java/com/myapp/dto/UserParams.java b/src/main/java/com/myapp/dto/UserParams.java deleted file mode 100644 index 72c5811..0000000 --- a/src/main/java/com/myapp/dto/UserParams.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.myapp.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.myapp.domain.User; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; - -import javax.validation.constraints.Size; -import java.util.Optional; - -@ToString -@EqualsAndHashCode -public final class UserParams { - - @SuppressWarnings("unused") - private static final Logger logger = LoggerFactory.getLogger(UserParams.class); - - private final String email; - @Size(min = 8, max = 100) - private final String password; - private final String name; - - public UserParams(@JsonProperty("email") String email, - @JsonProperty("password") String password, - @JsonProperty("name") String name) { - this.email = email; - this.password = password; - this.name = name; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public Optional getEncodedPassword() { - return Optional.ofNullable(password).map(p -> new BCryptPasswordEncoder().encode(p)); - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public User toUser() { - User user = new User(); - user.setUsername(email); - user.setPassword(new BCryptPasswordEncoder().encode(password)); - user.setName(name); - return user; - } - -} diff --git a/src/main/java/com/myapp/repository/MicropostCustomRepository.java b/src/main/java/com/myapp/repository/MicropostCustomRepository.java deleted file mode 100644 index 622cad8..0000000 --- a/src/main/java/com/myapp/repository/MicropostCustomRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.Micropost; -import com.myapp.domain.User; -import com.myapp.domain.UserStats; -import com.myapp.dto.PageParams; -import lombok.Builder; -import lombok.Value; -import org.springframework.data.repository.Repository; - -import java.util.List; - -public interface MicropostCustomRepository extends Repository { - - List findAsFeed(User user, PageParams pageParams); - - List findByUser(User user, PageParams pageParams); - - @Value - @Builder - class Row { - private final Micropost micropost; - private final UserStats userStats; - } - -} diff --git a/src/main/java/com/myapp/repository/MicropostCustomRepositoryImpl.java b/src/main/java/com/myapp/repository/MicropostCustomRepositoryImpl.java deleted file mode 100644 index a22a77e..0000000 --- a/src/main/java/com/myapp/repository/MicropostCustomRepositoryImpl.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.*; -import com.myapp.domain.QMicropost; -import com.myapp.domain.QRelationship; -import com.myapp.dto.PageParams; -import com.myapp.repository.helper.UserStatsQueryHelper; -import com.querydsl.core.types.ConstructorExpression; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.stream.Collectors; - -@Repository -public class MicropostCustomRepositoryImpl implements MicropostCustomRepository { - - private final JPAQueryFactory queryFactory; - - @Autowired - public MicropostCustomRepositoryImpl(JPAQueryFactory queryFactory) { - this.queryFactory = queryFactory; - } - - @Override - public List findAsFeed(User user, PageParams pageParams) { - final QMicropost qMicropost = QMicropost.micropost; - final QRelationship qRelationship = QRelationship.relationship; - - final ConstructorExpression userStatsExpression = - UserStatsQueryHelper.userStatsExpression(qMicropost.user); - final JPQLQuery relationshipSubQuery = JPAExpressions - .selectFrom(qRelationship) - .where(qRelationship.follower.eq(user) - .and(qRelationship.followed.eq(qMicropost.user)) - ); - return queryFactory.select(qMicropost, qMicropost.user, userStatsExpression) - .from(qMicropost) - .innerJoin(qMicropost.user) - .where((qMicropost.user.eq(user).or(relationshipSubQuery.exists())) - .and(pageParams.getSinceId().map(qMicropost.id::gt).orElse(null)) - .and(pageParams.getMaxId().map(qMicropost.id::lt).orElse(null)) - ) - .orderBy(qMicropost.id.desc()) - .limit(pageParams.getCount()) - .fetch() - .stream() - .map(tuple -> Row.builder() - .micropost(tuple.get(qMicropost)) - .userStats(tuple.get(userStatsExpression)) - .build() - ) - .collect(Collectors.toList()); - } - - @Override - public List findByUser(User user, PageParams pageParams) { - final QMicropost qMicropost = QMicropost.micropost; - return queryFactory.selectFrom(qMicropost) - .where(qMicropost.user.eq(user) - .and(pageParams.getSinceId().map(qMicropost.id::gt).orElse(null)) - .and(pageParams.getMaxId().map(qMicropost.id::lt).orElse(null)) - ) - .orderBy(qMicropost.id.desc()) - .limit(pageParams.getCount()) - .fetch() - .stream() - .map(post -> Row.builder() - .micropost(post) - .build() - ) - .collect(Collectors.toList()); - } - - -} diff --git a/src/main/java/com/myapp/repository/MicropostRepository.java b/src/main/java/com/myapp/repository/MicropostRepository.java deleted file mode 100644 index bda5358..0000000 --- a/src/main/java/com/myapp/repository/MicropostRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.Micropost; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface MicropostRepository extends JpaRepository { -} diff --git a/src/main/java/com/myapp/repository/RelatedUserCustomRepository.java b/src/main/java/com/myapp/repository/RelatedUserCustomRepository.java deleted file mode 100644 index 6d28313..0000000 --- a/src/main/java/com/myapp/repository/RelatedUserCustomRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.Relationship; -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.domain.UserStats; -import lombok.Builder; -import lombok.Value; -import org.springframework.data.repository.Repository; - -import java.util.List; - -public interface RelatedUserCustomRepository extends Repository { - - List findFollowings(User user, PageParams pageParams); - - List findFollowers(User user, PageParams pageParams); - - @Value - @Builder - class Row { - private final User user; - private final Relationship relationship; - private final UserStats userStats; - } -} diff --git a/src/main/java/com/myapp/repository/RelatedUserCustomRepositoryImpl.java b/src/main/java/com/myapp/repository/RelatedUserCustomRepositoryImpl.java deleted file mode 100644 index d2c11a4..0000000 --- a/src/main/java/com/myapp/repository/RelatedUserCustomRepositoryImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.QRelationship; -import com.myapp.domain.QUser; -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.domain.UserStats; -import com.myapp.repository.helper.UserStatsQueryHelper; -import com.querydsl.core.types.ConstructorExpression; -import com.querydsl.jpa.impl.JPAQueryFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.stream.Collectors; - -@Repository -public class RelatedUserCustomRepositoryImpl implements RelatedUserCustomRepository { - - private final JPAQueryFactory queryFactory; - - private final QUser qUser = QUser.user; - private final QRelationship qRelationship = QRelationship.relationship; - - @Autowired - public RelatedUserCustomRepositoryImpl(JPAQueryFactory queryFactory) { - this.queryFactory = queryFactory; - } - - @Override - public List findFollowings(User subject, PageParams pageParams) { - final ConstructorExpression userStatsExpression = - UserStatsQueryHelper.userStatsExpression(qUser); - - return queryFactory.select(qUser, qRelationship, userStatsExpression) - .from(qUser) - .innerJoin(qUser.followedRelations, qRelationship) - .where(qRelationship.follower.eq(subject) - .and(pageParams.getSinceId().map(qRelationship.id::gt).orElse(null)) - .and(pageParams.getMaxId().map(qRelationship.id::lt).orElse(null)) - ) - .orderBy(qRelationship.id.desc()) - .limit(pageParams.getCount()) - .fetch() - .stream() - .map(tuple -> Row.builder() - .user(tuple.get(qUser)) - .relationship(tuple.get(qRelationship)) - .userStats(tuple.get(userStatsExpression)) - .build() - ) - .collect(Collectors.toList()); - } - - @Override - public List findFollowers(User subject, PageParams pageParams) { - final ConstructorExpression userStatsExpression = - UserStatsQueryHelper.userStatsExpression(qUser); - - return queryFactory.select(qUser, qRelationship, userStatsExpression) - .from(qUser) - .innerJoin(qUser.followerRelations, qRelationship) - .where(qRelationship.followed.eq(subject) - .and(pageParams.getSinceId().map(qRelationship.id::gt).orElse(null)) - .and(pageParams.getMaxId().map(qRelationship.id::lt).orElse(null)) - ) - .orderBy(qRelationship.id.desc()) - .limit(pageParams.getCount()) - .fetch() - .stream() - .map(tuple -> Row.builder() - .user(tuple.get(qUser)) - .relationship(tuple.get(qRelationship)) - .userStats(tuple.get(userStatsExpression)) - .build() - ) - .collect(Collectors.toList()); - } - -} diff --git a/src/main/java/com/myapp/repository/RelationshipRepository.java b/src/main/java/com/myapp/repository/RelationshipRepository.java deleted file mode 100644 index c30b63e..0000000 --- a/src/main/java/com/myapp/repository/RelationshipRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.Relationship; -import com.myapp.domain.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.querydsl.QueryDslPredicateExecutor; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -public interface RelationshipRepository extends JpaRepository, QueryDslPredicateExecutor { - - Optional findOneByFollowerAndFollowed(User follower, User followed); - - Stream findAllByFollowerAndFollowedIn(User follower, List targets); - -} diff --git a/src/main/java/com/myapp/repository/UserCustomRepository.java b/src/main/java/com/myapp/repository/UserCustomRepository.java deleted file mode 100644 index ab3ff23..0000000 --- a/src/main/java/com/myapp/repository/UserCustomRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.User; -import com.myapp.domain.UserStats; -import lombok.Builder; -import lombok.Value; -import org.springframework.data.repository.Repository; - -import java.util.Optional; - -public interface UserCustomRepository extends Repository { - - Optional findOne(Long userId); - - @Value - @Builder - class Row { - private final User user; - private final UserStats userStats; - } -} diff --git a/src/main/java/com/myapp/repository/UserCustomRepositoryImpl.java b/src/main/java/com/myapp/repository/UserCustomRepositoryImpl.java deleted file mode 100644 index 2758ccd..0000000 --- a/src/main/java/com/myapp/repository/UserCustomRepositoryImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.QUser; -import com.myapp.domain.UserStats; -import com.myapp.repository.helper.UserStatsQueryHelper; -import com.querydsl.core.Tuple; -import com.querydsl.core.types.ConstructorExpression; -import com.querydsl.jpa.impl.JPAQueryFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -class UserCustomRepositoryImpl implements UserCustomRepository { - - private final JPAQueryFactory queryFactory; - - private final QUser qUser = QUser.user; - - @Autowired - public UserCustomRepositoryImpl(JPAQueryFactory queryFactory) { - this.queryFactory = queryFactory; - } - - @Override - public Optional findOne(Long userId) { - final ConstructorExpression userStatsExpression = - UserStatsQueryHelper.userStatsExpression(qUser); - final Tuple row = queryFactory.select(qUser, userStatsExpression) - .from(qUser) - .where(qUser.id.eq(userId)) - .fetchOne(); - return Optional.ofNullable(row).map(tuple -> - Row.builder() - .user(tuple.get(qUser)) - .userStats(tuple.get(userStatsExpression)) - .build() - ); - } - - -} diff --git a/src/main/java/com/myapp/repository/UserRepository.java b/src/main/java/com/myapp/repository/UserRepository.java deleted file mode 100644 index d518556..0000000 --- a/src/main/java/com/myapp/repository/UserRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.myapp.repository; - -import com.myapp.domain.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - - Optional findOneByUsername(String username); - -} diff --git a/src/main/java/com/myapp/repository/helper/UserStatsQueryHelper.java b/src/main/java/com/myapp/repository/helper/UserStatsQueryHelper.java deleted file mode 100644 index 4dd9959..0000000 --- a/src/main/java/com/myapp/repository/helper/UserStatsQueryHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.myapp.repository.helper; - -import com.myapp.domain.QMicropost; -import com.myapp.domain.QRelationship; -import com.myapp.domain.QUser; -import com.myapp.domain.UserStats; -import com.querydsl.core.types.ConstructorExpression; -import com.querydsl.core.types.Projections; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.JPQLQuery; - -public class UserStatsQueryHelper { - - public static ConstructorExpression userStatsExpression(QUser qUser) { - return Projections.constructor(UserStats.class, - cntPostsQuery(qUser), - cntFollowingsQuery(qUser), - cntFollowersQuery(qUser) - ); - } - - private static JPQLQuery cntFollowersQuery(QUser qUser) { - final QRelationship qRelationship = new QRelationship("relationship_cnt_followers"); - return JPAExpressions.select(qRelationship.count()) - .from(qRelationship) - .where(qRelationship.followed.eq(qUser)); - } - - private static JPQLQuery cntFollowingsQuery(QUser qUser) { - final QRelationship qRelationship = new QRelationship("relationship_cnt_followings"); - return JPAExpressions.select(qRelationship.count()) - .from(qRelationship) - .where(qRelationship.follower.eq(qUser)); - } - - private static JPQLQuery cntPostsQuery(QUser qUser) { - final QMicropost qMicropost = new QMicropost("micropost_cnt_posts"); - return JPAExpressions.select(qMicropost.count()) - .from(qMicropost) - .where(qMicropost.user.eq(qUser)); - } -} diff --git a/src/main/java/com/myapp/service/MicropostService.java b/src/main/java/com/myapp/service/MicropostService.java deleted file mode 100644 index f30628a..0000000 --- a/src/main/java/com/myapp/service/MicropostService.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.myapp.service; - -import com.myapp.domain.Micropost; -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.dto.PostDTO; -import com.myapp.service.exceptions.NotPermittedException; - -import java.util.List; - -public interface MicropostService { - - void delete(Long id) throws NotPermittedException; - - List findAsFeed(PageParams pageParams); - - List findByUser(Long userId, PageParams pageParams); - - List findMyPosts(PageParams pageParams); - - Micropost saveMyPost(Micropost post); - -} diff --git a/src/main/java/com/myapp/service/MicropostServiceImpl.java b/src/main/java/com/myapp/service/MicropostServiceImpl.java deleted file mode 100644 index 10770ac..0000000 --- a/src/main/java/com/myapp/service/MicropostServiceImpl.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.myapp.service; - -import com.myapp.domain.Micropost; -import com.myapp.domain.Relationship; -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.dto.PostDTO; -import com.myapp.repository.MicropostCustomRepository; -import com.myapp.repository.MicropostRepository; -import com.myapp.repository.RelationshipRepository; -import com.myapp.repository.UserRepository; -import com.myapp.service.exceptions.NotPermittedException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Service -@Transactional -public class MicropostServiceImpl implements MicropostService { - - private final MicropostRepository micropostRepository; - private final UserRepository userRepository; - private final MicropostCustomRepository micropostCustomRepository; - private final RelationshipRepository relationshipRepository; - private final SecurityContextService securityContextService; - - @Autowired - public MicropostServiceImpl(MicropostRepository micropostRepository, UserRepository userRepository, MicropostCustomRepository micropostCustomRepository, RelationshipRepository relationshipRepository, SecurityContextService securityContextService) { - this.micropostRepository = micropostRepository; - this.userRepository = userRepository; - this.micropostCustomRepository = micropostCustomRepository; - this.relationshipRepository = relationshipRepository; - this.securityContextService = securityContextService; - } - - @Override - public void delete(Long id) throws NotPermittedException { - final Micropost micropost = micropostRepository.findOne(id); - final Optional currentUser = securityContextService.currentUser(); - - currentUser.filter(u -> u.equals(micropost.getUser())) - .ifPresent(u -> micropostRepository.delete(id)); - currentUser.filter(u -> u.equals(micropost.getUser())) - .orElseThrow(() -> new NotPermittedException("no permission to delete this post")); - } - - @Override - public List findAsFeed(PageParams pageParams) { - return securityContextService.currentUser() - .map(u -> { - final List rows = micropostCustomRepository.findAsFeed(u, pageParams); - final List relatedUsers = rows.stream() - .map(row -> row.getMicropost().getUser()) - .collect(Collectors.toList()); - return rows.stream() - .map(toDTO(relatedUsers)) - .collect(Collectors.toList()); - }) - .orElseThrow(() -> new AccessDeniedException("")); - } - - @Override - public List findByUser(Long userId, PageParams pageParams) { - final User user = userRepository.findOne(userId); - final List relatedUsers = Collections.singletonList(user); - return micropostCustomRepository.findByUser(user, pageParams) - .stream() - .map(toDTO(relatedUsers)) - .collect(Collectors.toList()); - } - - @Override - public List findMyPosts(PageParams pageParams) { - return securityContextService.currentUser() - .map(u -> findByUser(u.getId(), pageParams)) - .orElseThrow(RuntimeException::new); - } - - @Override - public Micropost saveMyPost(Micropost post) { - return securityContextService.currentUser() - .map(u -> { - post.setUser(u); - return micropostRepository.save(post); - }) - .orElseThrow(() -> new AccessDeniedException("")); - } - - private Function toDTO(List relatedUsers) { - final Optional currentUser = securityContextService.currentUser(); - final List followedByMe = currentUser.map(u -> relationshipRepository - .findAllByFollowerAndFollowedIn(u, relatedUsers) - .map(Relationship::getFollowed) - .collect(Collectors.toList()) - ).orElse(Collections.emptyList()); - - return r -> { - final Boolean isMyPost = currentUser - .map(u -> r.getMicropost().getUser().equals(u)) - .orElse(null); - final Boolean isFollowedByMe = currentUser - .map(u -> followedByMe.contains(r.getMicropost().getUser())) - .orElse(null); - return PostDTO.newInstance(r.getMicropost(), r.getUserStats(), isMyPost, isFollowedByMe); - }; - } - -} diff --git a/src/main/java/com/myapp/service/RelationshipService.java b/src/main/java/com/myapp/service/RelationshipService.java deleted file mode 100644 index 7112bd4..0000000 --- a/src/main/java/com/myapp/service/RelationshipService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.myapp.service; - -import com.myapp.dto.PageParams; -import com.myapp.dto.RelatedUserDTO; -import com.myapp.service.exceptions.RelationshipNotFoundException; - -import java.util.List; - -public interface RelationshipService { - - List findFollowings(Long userId, PageParams pageParams); - - List findFollowers(Long userId, PageParams pageParams); - - void follow(Long userId); - - void unfollow(Long userId) throws RelationshipNotFoundException; - -} diff --git a/src/main/java/com/myapp/service/RelationshipServiceImpl.java b/src/main/java/com/myapp/service/RelationshipServiceImpl.java deleted file mode 100644 index 41f986c..0000000 --- a/src/main/java/com/myapp/service/RelationshipServiceImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.myapp.service; - -import com.myapp.Utils; -import com.myapp.domain.Relationship; -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.dto.RelatedUserDTO; -import com.myapp.repository.RelatedUserCustomRepository; -import com.myapp.repository.RelationshipRepository; -import com.myapp.repository.UserRepository; -import com.myapp.service.exceptions.RelationshipNotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -@Service -@Transactional -public class RelationshipServiceImpl implements RelationshipService { - - private final RelationshipRepository relationshipRepository; - private final RelatedUserCustomRepository relatedUserCustomRepository; - private final UserRepository userRepository; - private final SecurityContextService securityContextService; - - public RelationshipServiceImpl(RelationshipRepository relationshipRepository, - RelatedUserCustomRepository relatedUserCustomRepository, - UserRepository userRepository, - SecurityContextService securityContextService) { - this.relationshipRepository = relationshipRepository; - this.relatedUserCustomRepository = relatedUserCustomRepository; - this.userRepository = userRepository; - this.securityContextService = securityContextService; - } - - @Override - public List findFollowings(Long userId, PageParams pageParams) { - final User user = userRepository.findOne(userId); - final List rows = relatedUserCustomRepository.findFollowings(user, pageParams); - - return rowsToRelatedUsers(rows); - } - - @Override - public List findFollowers(Long userId, PageParams pageParams) { - final User user = userRepository.findOne(userId); - final List rows = relatedUserCustomRepository.findFollowers(user, pageParams); - - return rowsToRelatedUsers(rows); - } - - @Override - public void follow(Long userId) { - final User user = userRepository.findOne(userId); - securityContextService.currentUser().ifPresent(currentUser -> { - final Relationship relationship = new Relationship(currentUser, user); - relationshipRepository.save(relationship); - }); - } - - @Override - public void unfollow(Long userId) throws RelationshipNotFoundException { - final User followed = userRepository.findOne(userId); - final Optional relationship = securityContextService.currentUser() - .flatMap(currentUser -> relationshipRepository.findOneByFollowerAndFollowed(currentUser, followed)); - - relationship.ifPresent(relationshipRepository::delete); - relationship.orElseThrow(RelationshipNotFoundException::new); - } - - private List rowsToRelatedUsers(List rows) { - final Optional currentUser = securityContextService.currentUser(); - - final List relatedUsers = rows.stream() - .map(RelatedUserCustomRepository.Row::getUser) - .collect(Collectors.toList()); - - final List followedByMe = currentUser.map(u -> relationshipRepository - .findAllByFollowerAndFollowedIn(u, relatedUsers) - .map(Relationship::getFollowed) - .collect(Collectors.toList()) - ).orElse(Collections.emptyList()); - - return rows.stream().map(r -> { - final Boolean isFollowedByMe = currentUser - .map(u -> followedByMe.contains(r.getUser())) - .orElse(null); - return RelatedUserDTO.builder() - .id(r.getUser().getId()) - .avatarHash(Utils.md5(r.getUser().getUsername())) - .name(r.getUser().getName()) - .userStats(r.getUserStats()) - .relationshipId(r.getRelationship().getId()) - .isFollowedByMe(isFollowedByMe) - .build(); - }).collect(Collectors.toList()); - } - -} diff --git a/src/main/java/com/myapp/service/SecurityContextService.java b/src/main/java/com/myapp/service/SecurityContextService.java deleted file mode 100644 index 0e131a0..0000000 --- a/src/main/java/com/myapp/service/SecurityContextService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.myapp.service; - -import com.myapp.domain.User; - -import java.util.Optional; - -public interface SecurityContextService { - Optional currentUser(); -} diff --git a/src/main/java/com/myapp/service/SecurityContextServiceImpl.java b/src/main/java/com/myapp/service/SecurityContextServiceImpl.java deleted file mode 100644 index 9e4d672..0000000 --- a/src/main/java/com/myapp/service/SecurityContextServiceImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.myapp.service; - -import com.myapp.domain.User; -import com.myapp.repository.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@Transactional -public class SecurityContextServiceImpl implements SecurityContextService { - - private final UserRepository userRepository; - - @Autowired - public SecurityContextServiceImpl(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Override - public Optional currentUser() { - final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return userRepository.findOneByUsername(authentication.getName()); - } -} diff --git a/src/main/java/com/myapp/service/UserService.java b/src/main/java/com/myapp/service/UserService.java deleted file mode 100644 index acba3b7..0000000 --- a/src/main/java/com/myapp/service/UserService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.myapp.service; - -import com.myapp.domain.User; -import com.myapp.dto.PageParams; -import com.myapp.dto.RelatedUserDTO; -import com.myapp.dto.UserDTO; -import com.myapp.dto.UserParams; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; - -import java.util.List; -import java.util.Optional; - -public interface UserService extends org.springframework.security.core.userdetails.UserDetailsService { - - Optional findOne(Long id); - - Optional findMe(); - - Page findAll(PageRequest pageable); - - User create(UserParams params); - - User update(User user, UserParams params); - - User updateMe(UserParams params); - - -} diff --git a/src/main/java/com/myapp/service/UserServiceImpl.java b/src/main/java/com/myapp/service/UserServiceImpl.java deleted file mode 100644 index 037c589..0000000 --- a/src/main/java/com/myapp/service/UserServiceImpl.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.myapp.service; - -import com.myapp.Utils; -import com.myapp.domain.User; -import com.myapp.dto.UserDTO; -import com.myapp.dto.UserParams; -import com.myapp.repository.RelationshipRepository; -import com.myapp.repository.UserCustomRepository; -import com.myapp.repository.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.AccountStatusUserDetailsChecker; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service("userService") -@Transactional -public class UserServiceImpl implements UserService { - - private final UserRepository userRepository; - private final UserCustomRepository userCustomRepository; - private final RelationshipRepository relationshipRepository; - private final SecurityContextService securityContextService; - - @Autowired - public UserServiceImpl(UserRepository userRepository, - UserCustomRepository userCustomRepository, - RelationshipRepository relationshipRepository, - SecurityContextService securityContextService) { - this.userRepository = userRepository; - this.userCustomRepository = userCustomRepository; - this.relationshipRepository = relationshipRepository; - this.securityContextService = securityContextService; - } - - @Override - public Optional findOne(Long id) { - return userCustomRepository.findOne(id).map(r -> { - final Optional currentUser = securityContextService.currentUser(); - final Boolean isFollowedByMe = currentUser - .map(u -> relationshipRepository - .findOneByFollowerAndFollowed(u, r.getUser()) - .isPresent() - ) - .orElse(null); - // Set email only if it equals with currentUser. - final String email = currentUser - .filter(u -> u.equals(r.getUser())) - .map(User::getUsername) - .orElse(null); - - return UserDTO.builder() - .id(r.getUser().getId()) - .email(email) - .avatarHash(Utils.md5(r.getUser().getUsername())) - .name(r.getUser().getName()) - .userStats(r.getUserStats()) - .isFollowedByMe(isFollowedByMe) - .build(); - }); - } - - @Override - public Optional findMe() { - return securityContextService.currentUser().flatMap(u -> findOne(u.getId())); - } - - @Override - public Page findAll(PageRequest pageable) { - return userRepository.findAll(pageable).map(u -> UserDTO.builder() - .id(u.getId()) - .name(u.getName()) - .avatarHash(Utils.md5(u.getUsername())) - .build() - ); - } - - @Override - public User create(UserParams params) { - return userRepository.save(params.toUser()); - } - - @Override - public User update(User user, UserParams params) { - params.getEmail().ifPresent(user::setUsername); - params.getEncodedPassword().ifPresent(user::setPassword); - params.getName().ifPresent(user::setName); - return userRepository.save(user); - } - - @Override - public User updateMe(UserParams params) { - return securityContextService.currentUser() - .map(u -> update(u, params)) - .orElseThrow(() -> new AccessDeniedException("")); - } - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - final Optional user = userRepository.findOneByUsername(username); - final AccountStatusUserDetailsChecker detailsChecker = new AccountStatusUserDetailsChecker(); - user.ifPresent(detailsChecker::check); - return user.orElseThrow(() -> new UsernameNotFoundException("user not found.")); - } - -} diff --git a/src/main/java/com/myapp/service/exceptions/NotPermittedException.java b/src/main/java/com/myapp/service/exceptions/NotPermittedException.java deleted file mode 100644 index 53a5fa8..0000000 --- a/src/main/java/com/myapp/service/exceptions/NotPermittedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.myapp.service.exceptions; - -public class NotPermittedException extends Exception { - - public NotPermittedException(String message) { - super(message); - } - -} diff --git a/src/main/java/com/myapp/service/exceptions/RelationshipNotFoundException.java b/src/main/java/com/myapp/service/exceptions/RelationshipNotFoundException.java deleted file mode 100644 index 6c00c91..0000000 --- a/src/main/java/com/myapp/service/exceptions/RelationshipNotFoundException.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.myapp.service.exceptions; - -public class RelationshipNotFoundException extends Exception { -} diff --git a/src/main/java/com/myapp/service/exceptions/UserNotFoundException.java b/src/main/java/com/myapp/service/exceptions/UserNotFoundException.java deleted file mode 100644 index c2e5edc..0000000 --- a/src/main/java/com/myapp/service/exceptions/UserNotFoundException.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.myapp.service.exceptions; - -public class UserNotFoundException extends Exception { -} diff --git a/src/main/kotlin/com/myapp/Application.kt b/src/main/kotlin/com/myapp/Application.kt new file mode 100644 index 0000000..2215c0d --- /dev/null +++ b/src/main/kotlin/com/myapp/Application.kt @@ -0,0 +1,13 @@ +package com.myapp + +import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +@EnableEncryptableProperties +class Application + +fun main(args: Array) { + SpringApplication.run(Application::class.java, *args) +} diff --git a/src/main/kotlin/com/myapp/Utils.kt b/src/main/kotlin/com/myapp/Utils.kt new file mode 100644 index 0000000..a2e5909 --- /dev/null +++ b/src/main/kotlin/com/myapp/Utils.kt @@ -0,0 +1,10 @@ +package com.myapp + +import java.security.MessageDigest +import javax.xml.bind.DatatypeConverter + +fun String.md5(): String { + val digester = MessageDigest.getInstance("MD5") + val bytes = digester.digest(toByteArray()) + return DatatypeConverter.printHexBinary(bytes).toLowerCase() +} diff --git a/src/main/kotlin/com/myapp/auth/SecurityConfig.kt b/src/main/kotlin/com/myapp/auth/SecurityConfig.kt new file mode 100644 index 0000000..990bb37 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/SecurityConfig.kt @@ -0,0 +1,77 @@ +package com.myapp.auth + +import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.HttpMethod +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + + +@Configuration +@EnableWebSecurity +@Order(1) +class SecurityConfig( + private val userDetailsService: UserDetailsService, + private val statelessAuthenticationFilter: StatelessAuthenticationFilter +) : WebSecurityConfigurerAdapter(true) { + + override fun configure(http: HttpSecurity) { + // we use jwt so that we can disable csrf protection + http.csrf().disable() + + http + .exceptionHandling().and() + .anonymous().and() + .servletApi().and() + .headers().cacheControl() + + http.authorizeRequests() + .antMatchers(HttpMethod.GET, "/api/users").hasRole("USER") + .antMatchers(HttpMethod.GET, "/api/users/me").hasRole("USER") + .antMatchers(HttpMethod.PATCH, "/api/users/me").hasRole("USER") + .antMatchers(HttpMethod.GET, "/api/users/me/microposts").hasRole("USER") + .antMatchers(HttpMethod.POST, "/api/microposts/**").hasRole("USER") + .antMatchers(HttpMethod.DELETE, "/api/microposts/**").hasRole("USER") + .antMatchers(HttpMethod.POST, "/api/relationships/**").hasRole("USER") + .antMatchers(HttpMethod.DELETE, "/api/relationships/**").hasRole("USER") + .antMatchers(HttpMethod.GET, "/api/feed").hasRole("USER") + .and() + .exceptionHandling() + .authenticationEntryPoint(Http401AuthenticationEntryPoint("'Bearer token_type=\"JWT\"'")) + + http.addFilterBefore(statelessAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + } + + @Bean + override fun authenticationManagerBean(): AuthenticationManager = + super.authenticationManagerBean() + + /** + * Prevent StatelessAuthenticationFilter will be added to Spring Boot filter chain. + * Only Spring Security must use it. + */ + @Bean + fun registration(filter: StatelessAuthenticationFilter): FilterRegistrationBean { + return FilterRegistrationBean(filter).apply { + isEnabled = false + } + } + + override fun configure(auth: AuthenticationManagerBuilder) { + auth.userDetailsService(userDetailsService).passwordEncoder(BCryptPasswordEncoder()) + } + + override fun userDetailsService() = userDetailsService + +} + + diff --git a/src/main/kotlin/com/myapp/auth/SecurityContextService.kt b/src/main/kotlin/com/myapp/auth/SecurityContextService.kt new file mode 100644 index 0000000..8e069a3 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/SecurityContextService.kt @@ -0,0 +1,7 @@ +package com.myapp.auth + +import com.myapp.domain.User + +interface SecurityContextService { + fun currentUser(): User? +} diff --git a/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt b/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt new file mode 100644 index 0000000..65b7ce6 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt @@ -0,0 +1,30 @@ +package com.myapp.auth + +import com.myapp.domain.User +import com.myapp.domain.UserDetailsImpl +import org.slf4j.LoggerFactory +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class SecurityContextServiceImpl : SecurityContextService { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(SecurityContextServiceImpl::class.java) + + override fun currentUser(): User? { + return SecurityContextHolder + .getContext() + .authentication + .principal + .let { + when (it) { + is UserDetailsImpl -> it.user + else -> null + } + } + } + +} diff --git a/src/main/kotlin/com/myapp/auth/StatelessAuthenticationFilter.kt b/src/main/kotlin/com/myapp/auth/StatelessAuthenticationFilter.kt new file mode 100644 index 0000000..41d17d5 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/StatelessAuthenticationFilter.kt @@ -0,0 +1,34 @@ +package com.myapp.auth + +import io.jsonwebtoken.JwtException +import org.springframework.security.core.AuthenticationException +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.GenericFilterBean +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class StatelessAuthenticationFilter( + private val tokenAuthenticationService: TokenAuthenticationService +) : GenericFilterBean() { + + override fun doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) { + try { + val authentication = tokenAuthenticationService.authentication(req as HttpServletRequest) + SecurityContextHolder.getContext().authentication = authentication + chain.doFilter(req, res) + SecurityContextHolder.getContext().authentication = null + } catch (e: AuthenticationException) { + SecurityContextHolder.clearContext() + (res as HttpServletResponse).status = HttpServletResponse.SC_UNAUTHORIZED + } catch (e: JwtException) { + SecurityContextHolder.clearContext() + (res as HttpServletResponse).status = HttpServletResponse.SC_UNAUTHORIZED + } + } + +} diff --git a/src/main/kotlin/com/myapp/auth/TokenAuthenticationService.kt b/src/main/kotlin/com/myapp/auth/TokenAuthenticationService.kt new file mode 100644 index 0000000..4a6edb3 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/TokenAuthenticationService.kt @@ -0,0 +1,11 @@ +package com.myapp.auth + +import org.springframework.security.core.Authentication + +import javax.servlet.http.HttpServletRequest + +interface TokenAuthenticationService { + + fun authentication(request: HttpServletRequest): Authentication? +} + diff --git a/src/main/kotlin/com/myapp/auth/TokenAuthenticationServiceImpl.kt b/src/main/kotlin/com/myapp/auth/TokenAuthenticationServiceImpl.kt new file mode 100644 index 0000000..4e7131a --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/TokenAuthenticationServiceImpl.kt @@ -0,0 +1,24 @@ +package com.myapp.auth + +import org.springframework.security.core.Authentication +import org.springframework.stereotype.Service +import javax.servlet.http.HttpServletRequest + +@Service +class TokenAuthenticationServiceImpl( + private val tokenHandler: TokenHandler +) : TokenAuthenticationService { + + override fun authentication(request: HttpServletRequest): Authentication? { + val authHeader = request.getHeader("authorization") ?: return null + if (!authHeader.startsWith("Bearer")) return null + + val jwt = authHeader.substring(7) + if (jwt.isEmpty()) return null + + return tokenHandler + .parseUserFromToken(jwt) + .let(::UserAuthentication) + } +} + diff --git a/src/main/kotlin/com/myapp/auth/TokenHandler.kt b/src/main/kotlin/com/myapp/auth/TokenHandler.kt new file mode 100644 index 0000000..845a2e2 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/TokenHandler.kt @@ -0,0 +1,13 @@ +package com.myapp.auth + +import com.myapp.domain.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Component + +@Component +interface TokenHandler { + + fun parseUserFromToken(token: String): UserDetails + fun createTokenForUser(user: User): String + +} diff --git a/src/main/kotlin/com/myapp/auth/TokenHandlerImpl.kt b/src/main/kotlin/com/myapp/auth/TokenHandlerImpl.kt new file mode 100644 index 0000000..f96f6d6 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/TokenHandlerImpl.kt @@ -0,0 +1,43 @@ +package com.myapp.auth + +import com.myapp.domain.User +import com.myapp.domain.UserDetailsImpl +import com.myapp.repository.UserRepository +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import org.springframework.beans.factory.annotation.Value +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.stereotype.Component +import java.time.ZonedDateTime +import java.util.* + +@Component +class TokenHandlerImpl( + @param:Value("\${app.jwt.secret}") + private val secret: String, + private val userRepository: UserRepository +) : TokenHandler { + + override fun parseUserFromToken(token: String): UserDetails { + val userId = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .body + .subject + .toLong() + + return userRepository.findOne(userId).let(::UserDetailsImpl) + } + + override fun createTokenForUser(user: User): String { + val afterOneWeek = ZonedDateTime.now().plusWeeks(1) + + return Jwts.builder() + .setSubject(user.id.toString()) + .signWith(SignatureAlgorithm.HS512, secret) + .setExpiration(Date.from(afterOneWeek.toInstant())) + .compact() + } + +} + diff --git a/src/main/kotlin/com/myapp/auth/UserAuthentication.kt b/src/main/kotlin/com/myapp/auth/UserAuthentication.kt new file mode 100644 index 0000000..72fccf7 --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/UserAuthentication.kt @@ -0,0 +1,23 @@ +package com.myapp.auth + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class UserAuthentication( + private val user: UserDetails +) : Authentication { + private var authenticated = true + + override fun getAuthorities(): Collection = user.authorities + override fun getCredentials(): Any = user.password + override fun getDetails(): Any? = null + override fun getPrincipal(): UserDetails = user + override fun isAuthenticated(): Boolean = authenticated + override fun getName(): String = user.username + + override fun setAuthenticated(authenticated: Boolean) { + this.authenticated = authenticated + } + +} diff --git a/src/main/kotlin/com/myapp/config/Swagger2Config.kt b/src/main/kotlin/com/myapp/config/Swagger2Config.kt new file mode 100644 index 0000000..79704dc --- /dev/null +++ b/src/main/kotlin/com/myapp/config/Swagger2Config.kt @@ -0,0 +1,25 @@ +package com.myapp.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import springfox.documentation.spi.DocumentationType +import springfox.documentation.spring.web.plugins.Docket +import springfox.documentation.swagger2.annotations.EnableSwagger2 + +import springfox.documentation.builders.PathSelectors.regex + +@Configuration +@Profile("dev") +@EnableSwagger2 +class Swagger2Config { + + @Bean + fun swaggerSpringMvcPlugin(): Docket { + return Docket(DocumentationType.SWAGGER_2) + .select() + .paths(regex("/api.*")) + .build() + } + +} diff --git a/src/main/kotlin/com/myapp/controller/AuthController.kt b/src/main/kotlin/com/myapp/controller/AuthController.kt new file mode 100644 index 0000000..8a66f12 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/AuthController.kt @@ -0,0 +1,44 @@ +package com.myapp.controller + +import com.myapp.auth.TokenHandler +import com.myapp.auth.SecurityContextService +import org.slf4j.LoggerFactory +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/api/auth") +class AuthController( + private val authenticationManager: AuthenticationManager, + private val tokenHandler: TokenHandler, + private val securityContextService: SecurityContextService +) { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(AuthController::class.java) + + @PostMapping + fun auth(@RequestBody params: AuthParams): AuthResponse { + val loginToken = UsernamePasswordAuthenticationToken(params.email, params.password) + val authentication = authenticationManager.authenticate(loginToken) + SecurityContextHolder.getContext().authentication = authentication + + return securityContextService.currentUser() + .let { requireNotNull(it) } + .let { tokenHandler.createTokenForUser(it).let(::AuthResponse) } + } + + data class AuthParams( + val email: String, + val password: String + ) + + data class AuthResponse(val token: String) + +} diff --git a/src/main/kotlin/com/myapp/controller/FeedController.kt b/src/main/kotlin/com/myapp/controller/FeedController.kt new file mode 100644 index 0000000..edf85a3 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/FeedController.kt @@ -0,0 +1,30 @@ +package com.myapp.controller + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.service.FeedService +import org.slf4j.LoggerFactory +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/feed") +class FeedController( + private val feedService: FeedService +) { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(FeedController::class.java) + + @GetMapping + fun feed( + @RequestParam(required = false) sinceId: Long?, + @RequestParam(required = false) maxId: Long?, + @RequestParam(required = false, defaultValue = "20") count: Int + ): List { + return feedService.findFeed(PageParams(sinceId, maxId, count)) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/controller/MicropostController.kt b/src/main/kotlin/com/myapp/controller/MicropostController.kt new file mode 100644 index 0000000..1fe02d9 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/MicropostController.kt @@ -0,0 +1,34 @@ +package com.myapp.controller + +import com.myapp.domain.Micropost +import com.myapp.service.MicropostService +import com.myapp.service.exception.NotAuthorizedException +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + + +@RestController +@RequestMapping("/api/microposts") +class MicropostController( + private val micropostService: MicropostService +) { + + @PostMapping + fun create(@RequestBody params: MicropostParams): Micropost { + return micropostService.create(params.content) + } + + @DeleteMapping(value = "{id}") + fun delete(@PathVariable("id") id: Long) { + micropostService.delete(id) + } + + @ResponseStatus(value = HttpStatus.FORBIDDEN) + @ExceptionHandler(NotAuthorizedException::class) + fun handleNotAuthorized() = Unit + + data class MicropostParams(val content: String) + +} + + diff --git a/src/main/kotlin/com/myapp/controller/RelatedUserController.kt b/src/main/kotlin/com/myapp/controller/RelatedUserController.kt new file mode 100644 index 0000000..a2c1086 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/RelatedUserController.kt @@ -0,0 +1,36 @@ +package com.myapp.controller + +import com.myapp.domain.RelatedUser +import com.myapp.dto.request.PageParams +import com.myapp.service.RelatedUserService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/users/{userId}") +class RelatedUserController( + private val relatedUserService: RelatedUserService +) { + + @GetMapping(path = arrayOf("/followings")) + fun followings( + @PathVariable("userId") userId: Long, + @RequestParam(required = false) sinceId: Long?, + @RequestParam(required = false) maxId: Long?, + @RequestParam(required = false, defaultValue = "20") count: Int + ): List { + val pageParams = PageParams(sinceId, maxId, count) + return relatedUserService.findFollowings(userId, pageParams) + } + + @GetMapping(path = arrayOf("/followers")) + fun followers( + @PathVariable("userId") userId: Long, + @RequestParam(required = false) sinceId: Long?, + @RequestParam(required = false) maxId: Long?, + @RequestParam(required = false, defaultValue = "20") count: Int + ): List { + val pageParams = PageParams(sinceId, maxId, count) + return relatedUserService.findFollowers(userId, pageParams) + } + +} diff --git a/src/main/kotlin/com/myapp/controller/RelationshipController.kt b/src/main/kotlin/com/myapp/controller/RelationshipController.kt new file mode 100644 index 0000000..31f5a9d --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/RelationshipController.kt @@ -0,0 +1,35 @@ +package com.myapp.controller + +import com.myapp.repository.exception.RelationshipDuplicatedException +import com.myapp.service.RelationshipService +import com.myapp.service.exception.RelationshipNotFoundException +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/relationships") +class RelationshipController( + private val relationshipService: RelationshipService +) { + + @PostMapping(value = "/to/{followedId}") + fun follow(@PathVariable("followedId") followedId: Long) { + relationshipService.follow(followedId) + } + + @DeleteMapping(value = "/to/{followedId}") + fun unfollow(@PathVariable("followedId") followedId: Long) { + relationshipService.unfollow(followedId) + } + + @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "already followed") + @ExceptionHandler(RelationshipDuplicatedException::class) + fun handleRelationshipDuplicated() { + } + + @ResponseStatus(value = HttpStatus.NOT_FOUND) + @ExceptionHandler(RelationshipNotFoundException::class) + fun handleRelationshipNotFound() { + } + +} diff --git a/src/main/kotlin/com/myapp/controller/UserController.kt b/src/main/kotlin/com/myapp/controller/UserController.kt new file mode 100644 index 0000000..0834820 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/UserController.kt @@ -0,0 +1,59 @@ +package com.myapp.controller + +import com.myapp.domain.User +import com.myapp.dto.page.Page +import com.myapp.dto.request.UserEditParams +import com.myapp.dto.request.UserNewParams +import com.myapp.dto.response.ErrorResponse +import com.myapp.repository.exception.EmailDuplicatedException +import com.myapp.repository.exception.RecordNotFoundException +import com.myapp.service.UserService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* +import javax.validation.Valid + + +@RestController +@RequestMapping("/api/users") +class UserController( + private val userService: UserService +) { + + @GetMapping + fun list( + @RequestParam(value = "page", required = false) page: Int?, + @RequestParam(value = "size", required = false) size: Int? + ): Page { + return userService.findAll( + page = page ?: 1, + size = size ?: 5 + ) + } + + @PostMapping + fun create(@Valid @RequestBody params: UserNewParams): User { + return userService.create(params) + } + + @GetMapping(path = arrayOf("{id:\\d+}")) + fun show(@PathVariable("id") id: Long) = userService.findOne(id) + + @GetMapping(path = arrayOf("/me")) + fun showMe() = userService.findMe() + + @PatchMapping(path = arrayOf("/me")) + fun updateMe(@Valid @RequestBody params: UserEditParams) = userService.updateMe(params) + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(EmailDuplicatedException::class) + fun handleEmailDuplicatedException(e: EmailDuplicatedException): ErrorResponse { + return ErrorResponse("email_already_taken", "This email is already taken.") + } + + @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No user") + @ExceptionHandler(RecordNotFoundException::class) + fun handleUserNotFound() { + } + +} + diff --git a/src/main/kotlin/com/myapp/controller/UserMicropostController.kt b/src/main/kotlin/com/myapp/controller/UserMicropostController.kt new file mode 100644 index 0000000..a6912e6 --- /dev/null +++ b/src/main/kotlin/com/myapp/controller/UserMicropostController.kt @@ -0,0 +1,35 @@ +package com.myapp.controller + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.service.MicropostService +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/users") +class UserMicropostController( + private val micropostService: MicropostService +) { + + @GetMapping(path = arrayOf("/{userId:\\d+}/microposts")) + fun list( + @PathVariable("userId") userId: Long, + @RequestParam(required = false) sinceId: Long?, + @RequestParam(required = false) maxId: Long?, + @RequestParam(required = false, defaultValue = "20") count: Int + ): List { + val pageParams = PageParams(sinceId, maxId, count) + return micropostService.findAllByUser(userId, pageParams) + } + + @GetMapping(path = arrayOf("/me/microposts")) + fun listMyPosts( + @RequestParam(required = false) sinceId: Long?, + @RequestParam(required = false) maxId: Long?, + @RequestParam(required = false, defaultValue = "20") count: Int + ): List { + val pageParams = PageParams(sinceId, maxId, count) + return micropostService.findMyPosts(pageParams) + } + +} diff --git a/src/main/kotlin/com/myapp/domain/HasIdentity.kt b/src/main/kotlin/com/myapp/domain/HasIdentity.kt new file mode 100644 index 0000000..6b4258e --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/HasIdentity.kt @@ -0,0 +1,13 @@ +package com.myapp.domain + +import com.fasterxml.jackson.annotation.JsonIgnore + +interface HasIdentity { + + @get:JsonIgnore + val _id: T? + + val id: T + get() = _id ?: throw RuntimeException("This model is not saved yet.") + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/domain/Micropost.kt b/src/main/kotlin/com/myapp/domain/Micropost.kt new file mode 100644 index 0000000..60620ec --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/Micropost.kt @@ -0,0 +1,37 @@ +package com.myapp.domain + +import com.fasterxml.jackson.annotation.JsonProperty +import com.myapp.generated.tables.records.FeedRecord +import com.myapp.generated.tables.records.MicropostRecord +import java.util.* + +data class Micropost( + + override val _id: Long? = null, + + val content: String, + + val createdAt: Date = Date(), + + val user: User, + + @get:JsonProperty("isMyPost") + val isMyPost: Boolean? = null // null means unknown + +) : HasIdentity { + + constructor(record: MicropostRecord, user: User) : this( + _id = record.id, + content = record.content, + createdAt = record.createdAt, + user = user + ) + + constructor(record: FeedRecord, user: User) : this( + _id = record.id, + content = record.content, + createdAt = record.createdAt, + user = user + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/domain/RelatedUser.kt b/src/main/kotlin/com/myapp/domain/RelatedUser.kt new file mode 100644 index 0000000..49c5276 --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/RelatedUser.kt @@ -0,0 +1,29 @@ +package com.myapp.domain + +import com.fasterxml.jackson.annotation.JsonGetter +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.myapp.md5 + +data class RelatedUser( + + override val _id: Long? = null, + + val relationshipId: Long, + + @get:JsonIgnore + val username: String, + + val name: String, + + val userStats: UserStats? = null, + + @get:JsonProperty("isFollowedByMe") + val isFollowedByMe: Boolean? = null // null means unknown + +) : HasIdentity { + + @JsonGetter + fun avatarHash(): String = username.md5() + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/domain/Relationship.kt b/src/main/kotlin/com/myapp/domain/Relationship.kt new file mode 100644 index 0000000..bf0e9fd --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/Relationship.kt @@ -0,0 +1,7 @@ +package com.myapp.domain + +data class Relationship( + override val _id: Long? = null, + val follower: User, + val followed: User +) : HasIdentity diff --git a/src/main/kotlin/com/myapp/domain/User.kt b/src/main/kotlin/com/myapp/domain/User.kt new file mode 100644 index 0000000..94a33c7 --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/User.kt @@ -0,0 +1,55 @@ +package com.myapp.domain + +import com.fasterxml.jackson.annotation.JsonGetter +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.myapp.generated.tables.records.UserRecord +import com.myapp.md5 +import javax.validation.constraints.Pattern +import javax.validation.constraints.Size + +data class User( + + // ------- DB fields ------- + + override val _id: Long? = null, + + @get:JsonIgnore + @get:Pattern(regexp = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$") + @get:Size(min = 4, max = 30) + val username: String, + + @get:JsonIgnore + val password: String, + + @get:Size(min = 4, max = 30) + val name: String, + + // ------- Others ------- + + val userStats: UserStats? = null, + + @get:JsonProperty("isFollowedByMe") + val isFollowedByMe: Boolean? = null, // null means unknown + + @get:JsonIgnore + val isMyself: Boolean? = null // null means unknown + +) : HasIdentity { + + constructor(record: UserRecord) : this( + _id = record.id, + name = record.name, + username = record.username, + password = record.password + ) + + @JsonGetter + fun email(): String? { + return isMyself?.let { if (it) username else null } + } + + @JsonGetter + fun avatarHash(): String = username.md5() + +} diff --git a/src/main/kotlin/com/myapp/domain/UserDetailsImpl.kt b/src/main/kotlin/com/myapp/domain/UserDetailsImpl.kt new file mode 100644 index 0000000..311d268 --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/UserDetailsImpl.kt @@ -0,0 +1,16 @@ +package com.myapp.domain + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class UserDetailsImpl(val user: User) : UserDetails { + + override fun getUsername() = user.username + override fun getPassword() = user.password + override fun getAuthorities() = mutableListOf(GrantedAuthority { "ROLE_USER" }) + override fun isEnabled() = true + override fun isCredentialsNonExpired() = true + override fun isAccountNonExpired() = true + override fun isAccountNonLocked() = true + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/domain/UserStats.kt b/src/main/kotlin/com/myapp/domain/UserStats.kt new file mode 100644 index 0000000..7fc3983 --- /dev/null +++ b/src/main/kotlin/com/myapp/domain/UserStats.kt @@ -0,0 +1,15 @@ +package com.myapp.domain + +import com.myapp.generated.tables.records.UserStatsRecord + +data class UserStats( + val micropostCnt: Long, + val followingCnt: Long, + val followerCnt: Long +) { + constructor(record: UserStatsRecord) : this( + micropostCnt = record.micropostCnt, + followerCnt = record.followerCnt, + followingCnt = record.followingCnt + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/dto/page/Page.kt b/src/main/kotlin/com/myapp/dto/page/Page.kt new file mode 100644 index 0000000..2c1999e --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/page/Page.kt @@ -0,0 +1,6 @@ +package com.myapp.dto.page + +interface Page { + val totalPages: Int + val content: List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/dto/page/PageImpl.kt b/src/main/kotlin/com/myapp/dto/page/PageImpl.kt new file mode 100644 index 0000000..d1dca73 --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/page/PageImpl.kt @@ -0,0 +1,6 @@ +package com.myapp.dto.page + +data class PageImpl( + override val content: List, + override val totalPages: Int +) : Page \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/dto/request/PageParams.kt b/src/main/kotlin/com/myapp/dto/request/PageParams.kt new file mode 100644 index 0000000..ab2a68c --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/request/PageParams.kt @@ -0,0 +1,7 @@ +package com.myapp.dto.request + +data class PageParams( + val sinceId: Long? = null, + val maxId: Long? = null, + val count: Int = 20 +) \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/dto/request/UserEditParams.kt b/src/main/kotlin/com/myapp/dto/request/UserEditParams.kt new file mode 100644 index 0000000..c0d0d04 --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/request/UserEditParams.kt @@ -0,0 +1,13 @@ +package com.myapp.dto.request + +import javax.validation.constraints.Size + +data class UserEditParams( + + val email: String? = null, + + @get:Size(min = 8, max = 10) + val password: String? = null, + + val name: String? = null +) diff --git a/src/main/kotlin/com/myapp/dto/request/UserNewParams.kt b/src/main/kotlin/com/myapp/dto/request/UserNewParams.kt new file mode 100644 index 0000000..9c09723 --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/request/UserNewParams.kt @@ -0,0 +1,11 @@ +package com.myapp.dto.request + +import javax.validation.constraints.Size + +data class UserNewParams( + val email: String, + @get:Size(min = 8, max = 10) + val password: String, + val name: String +) + diff --git a/src/main/kotlin/com/myapp/dto/response/ErrorResponse.kt b/src/main/kotlin/com/myapp/dto/response/ErrorResponse.kt new file mode 100644 index 0000000..6f88012 --- /dev/null +++ b/src/main/kotlin/com/myapp/dto/response/ErrorResponse.kt @@ -0,0 +1,6 @@ +package com.myapp.dto.response + +data class ErrorResponse( + val code: String, + val message: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/FeedRepository.kt b/src/main/kotlin/com/myapp/repository/FeedRepository.kt new file mode 100644 index 0000000..cdda1f8 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/FeedRepository.kt @@ -0,0 +1,8 @@ +package com.myapp.repository + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams + +interface FeedRepository { + fun findFeed(userId: Long, pageParams: PageParams): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/FeedRepositoryImpl.kt b/src/main/kotlin/com/myapp/repository/FeedRepositoryImpl.kt new file mode 100644 index 0000000..ae10193 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/FeedRepositoryImpl.kt @@ -0,0 +1,36 @@ +package com.myapp.repository + +import com.myapp.domain.Micropost +import com.myapp.domain.User +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Feed.FEED +import com.myapp.generated.tables.User.USER +import org.jooq.DSLContext +import org.jooq.Record +import org.springframework.stereotype.Repository + +@Repository +class FeedRepositoryImpl( + private val dsl: DSLContext +) : FeedRepository, Pager { + + override fun findFeed(userId: Long, pageParams: PageParams): List = + dsl.select() + .from(FEED) + .join(USER) + .on(FEED.USER_ID.eq(USER.ID)) + .where(FEED.FEED_USER_ID.eq(userId)) + .and(pageParams.toCondition(FEED.ID)) + .orderBy(FEED.ID.desc()) + .limit(pageParams.count) + .fetch(mapper()) + + private fun mapper() = { r: Record -> + Micropost( + record = r.into(FEED), + user = User(r.into(USER)) + ) + } + +} + diff --git a/src/main/kotlin/com/myapp/repository/MicropostRepository.kt b/src/main/kotlin/com/myapp/repository/MicropostRepository.kt new file mode 100644 index 0000000..e9283f4 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/MicropostRepository.kt @@ -0,0 +1,12 @@ +package com.myapp.repository + +import com.myapp.domain.Micropost +import com.myapp.domain.User +import com.myapp.dto.request.PageParams + +interface MicropostRepository { + fun create(micropost: Micropost): Micropost + fun delete(id: Long) + fun findOne(id: Long): Micropost + fun findAllByUser(userId: Long, pageParams: PageParams): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/MicropostRepositoryImpl.kt b/src/main/kotlin/com/myapp/repository/MicropostRepositoryImpl.kt new file mode 100644 index 0000000..35fe49e --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/MicropostRepositoryImpl.kt @@ -0,0 +1,65 @@ +package com.myapp.repository + +import com.myapp.domain.Micropost +import com.myapp.domain.User +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Micropost.MICROPOST +import com.myapp.generated.tables.User.USER +import com.myapp.repository.exception.RecordNotFoundException +import org.jooq.DSLContext +import org.jooq.Record +import org.springframework.stereotype.Repository + +@Repository +class MicropostRepositoryImpl( + private val dsl: DSLContext +) : MicropostRepository, Pager { + + override fun findOne(id: Long) = + dsl.select() + .from(MICROPOST) + .join(USER) + .on(MICROPOST.USER_ID.eq(USER.ID)) + .where(MICROPOST.ID.eq(id)) + .fetchOne(mapper()) ?: throw RecordNotFoundException() + + override fun findAllByUser(userId: Long, pageParams: PageParams): List { + return dsl.select() + .from(MICROPOST) + .join(USER) + .on(MICROPOST.USER_ID.eq(USER.ID)) + .where(MICROPOST.USER_ID.eq(userId)) + .and(pageParams.toCondition(MICROPOST.ID)) + .orderBy(MICROPOST.ID.desc()) + .limit(pageParams.count) + .fetch(mapper()) + } + + override fun create(micropost: Micropost) = + dsl.insertInto(MICROPOST, MICROPOST.CONTENT, MICROPOST.USER_ID) + .values(micropost.content, micropost.user.id) + .returning() + .fetchOne() + .let(mapper(micropost)) + + override fun delete(id: Long) { + dsl.deleteFrom(MICROPOST) + .where(MICROPOST.ID.eq(id)) + .execute() + } + + private fun mapper() = { r: Record -> + Micropost( + record = r.into(MICROPOST), + user = User(r.into(USER)) + ) + } + + private fun mapper(original: Micropost) = { r: Record -> + Micropost( + record = r.into(MICROPOST), + user = original.user + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/Pager.kt b/src/main/kotlin/com/myapp/repository/Pager.kt new file mode 100644 index 0000000..d50456d --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/Pager.kt @@ -0,0 +1,17 @@ +package com.myapp.repository + +import com.myapp.dto.request.PageParams +import org.jooq.Condition +import org.jooq.Record +import org.jooq.TableField +import org.jooq.impl.DSL + +interface Pager { + + fun PageParams.toCondition(tableField: TableField): Condition { + return DSL.trueCondition() + .let { if (sinceId == null) it else it.and(tableField.gt(sinceId)) } + .let { if (maxId == null) it else it.and(tableField.lt(maxId)) } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/RelatedUserRepository.kt b/src/main/kotlin/com/myapp/repository/RelatedUserRepository.kt new file mode 100644 index 0000000..23b9df0 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/RelatedUserRepository.kt @@ -0,0 +1,9 @@ +package com.myapp.repository + +import com.myapp.domain.RelatedUser +import com.myapp.dto.request.PageParams + +interface RelatedUserRepository { + fun findFollowers(userId: Long, pageParams: PageParams): List + fun findFollowings(userId: Long, pageParams: PageParams): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/RelatedUserRepositoryImpl.kt b/src/main/kotlin/com/myapp/repository/RelatedUserRepositoryImpl.kt new file mode 100644 index 0000000..1693df3 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/RelatedUserRepositoryImpl.kt @@ -0,0 +1,60 @@ +package com.myapp.repository + +import com.myapp.domain.RelatedUser +import com.myapp.domain.UserStats +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Relationship.RELATIONSHIP +import com.myapp.generated.tables.User.USER +import com.myapp.generated.tables.UserStats.USER_STATS +import org.jooq.DSLContext +import org.jooq.Record +import org.springframework.stereotype.Repository +import com.myapp.generated.tables.User as UserTable + +@Repository +class RelatedUserRepositoryImpl( + private val dsl: DSLContext +) : RelatedUserRepository, Pager { + + override fun findFollowers(userId: Long, pageParams: PageParams): List { + return dsl.select() + .from(USER) + .join(RELATIONSHIP) + .on(USER.ID.eq(RELATIONSHIP.FOLLOWER_ID)) + .join(USER_STATS) + .on(USER.ID.eq(USER_STATS.USER_ID)) + .where(RELATIONSHIP.FOLLOWED_ID.eq(userId)) + .and(pageParams.toCondition(RELATIONSHIP.ID)) + .orderBy(RELATIONSHIP.ID.desc()) + .limit(pageParams.count) + .fetch(mapper()) + } + + override fun findFollowings(userId: Long, pageParams: PageParams): List { + return dsl.select() + .from(USER) + .join(RELATIONSHIP) + .on(USER.ID.eq(RELATIONSHIP.FOLLOWED_ID)) + .join(USER_STATS) + .on(USER.ID.eq(USER_STATS.USER_ID)) + .where(RELATIONSHIP.FOLLOWER_ID.eq(userId)) + .and(pageParams.toCondition(RELATIONSHIP.ID)) + .orderBy(RELATIONSHIP.ID.desc()) + .limit(pageParams.count) + .fetch(mapper()) + } + + private fun mapper() = { r: Record -> + val userRecord = r.into(USER) + val relationshipRecord = r.into(RELATIONSHIP) + val userStatsRecord = r.into(USER_STATS) + RelatedUser( + _id = userRecord.id, + username = userRecord.username, + name = userRecord.name, + relationshipId = relationshipRecord.id, + userStats = UserStats(userStatsRecord) + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/RelationshipRepository.kt b/src/main/kotlin/com/myapp/repository/RelationshipRepository.kt new file mode 100644 index 0000000..21b9395 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/RelationshipRepository.kt @@ -0,0 +1,12 @@ +package com.myapp.repository + +import com.myapp.domain.Relationship + +interface RelationshipRepository { + + fun findOneByFollowerAndFollowed(followerId: Long, followedId: Long): Relationship? + fun findAllByFollowerAndFollowedUsers(followerId: Long, userIds: List): List + fun create(relationship: Relationship): Relationship + fun delete(id: Long) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/RelationshipRepositoryImpl.kt b/src/main/kotlin/com/myapp/repository/RelationshipRepositoryImpl.kt new file mode 100644 index 0000000..f01afc4 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/RelationshipRepositoryImpl.kt @@ -0,0 +1,81 @@ +package com.myapp.repository + +import com.myapp.domain.Relationship +import com.myapp.domain.User +import com.myapp.generated.tables.Relationship.RELATIONSHIP +import com.myapp.generated.tables.User.USER +import com.myapp.repository.exception.RelationshipDuplicatedException +import org.jooq.DSLContext +import org.jooq.Record +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.stereotype.Repository +import com.myapp.generated.tables.User as UserTable + +@Repository +class RelationshipRepositoryImpl( + private val dsl: DSLContext +) : RelationshipRepository { + + private val FOLLOWER: UserTable = USER.`as`("follower") + private val FOLLOWED: UserTable = USER.`as`("followed") + + override fun findOneByFollowerAndFollowed(followerId: Long, followedId: Long): Relationship? { + return dsl.select() + .from(RELATIONSHIP) + .join(FOLLOWER) + .on(RELATIONSHIP.FOLLOWER_ID.eq(FOLLOWER.ID)) + .join(FOLLOWED) + .on(RELATIONSHIP.FOLLOWED_ID.eq(FOLLOWED.ID)) + .where(RELATIONSHIP.FOLLOWER_ID.eq(followerId)) + .and(RELATIONSHIP.FOLLOWED_ID.eq(followedId)) + .fetchOne(mapper()) + } + + override fun findAllByFollowerAndFollowedUsers(followerId: Long, userIds: List): List { + return dsl.select() + .from(RELATIONSHIP) + .join(FOLLOWER) + .on(RELATIONSHIP.FOLLOWER_ID.eq(FOLLOWER.ID)) + .join(FOLLOWED) + .on(RELATIONSHIP.FOLLOWED_ID.eq(FOLLOWED.ID)) + .where(RELATIONSHIP.FOLLOWER_ID.eq(followerId)) + .and(RELATIONSHIP.FOLLOWED_ID.`in`(userIds)) + .orderBy(RELATIONSHIP.ID.asc()) + .fetch(mapper()) + } + + override fun create(relationship: Relationship): Relationship { + try { + return dsl.insertInto(RELATIONSHIP, RELATIONSHIP.FOLLOWED_ID, RELATIONSHIP.FOLLOWER_ID) + .values(relationship.followed.id, relationship.follower.id) + .returning() + .fetchOne() + .let(mapper(relationship)) + } catch(e: DataIntegrityViolationException) { + throw RelationshipDuplicatedException("") + } + } + + override fun delete(id: Long) { + dsl.deleteFrom(RELATIONSHIP) + .where(RELATIONSHIP.ID.eq(id)) + .execute() + } + + private fun mapper() = { r: Record -> + Relationship( + _id = r.into(RELATIONSHIP).id, + followed = User(r.into(FOLLOWED)), + follower = User(r.into(FOLLOWER)) + ) + } + + private fun mapper(original: Relationship) = { r: Record -> + Relationship( + _id = r.into(RELATIONSHIP).id, + followed = original.followed, + follower = original.follower + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/UserRepository.kt b/src/main/kotlin/com/myapp/repository/UserRepository.kt new file mode 100644 index 0000000..4e6d728 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/UserRepository.kt @@ -0,0 +1,13 @@ +package com.myapp.repository + +import com.myapp.domain.User +import com.myapp.dto.page.Page + +interface UserRepository { + fun findOne(id: Long): User + fun findOneWithStats(id: Long): User + fun findOneByUsername(username: String): User? + fun findAll(page: Int, size: Int): Page + fun create(user: User): User + fun update(user: User) +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/UserRepositoryImpl.kt b/src/main/kotlin/com/myapp/repository/UserRepositoryImpl.kt new file mode 100644 index 0000000..c3529a5 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/UserRepositoryImpl.kt @@ -0,0 +1,111 @@ +package com.myapp.repository + +import com.myapp.domain.User +import com.myapp.domain.UserStats +import com.myapp.dto.page.Page +import com.myapp.dto.page.PageImpl +import com.myapp.generated.tables.User.USER +import com.myapp.generated.tables.UserStats.USER_STATS +import com.myapp.generated.tables.records.UserRecord +import com.myapp.repository.exception.EmailDuplicatedException +import com.myapp.repository.exception.RecordInvalidException +import com.myapp.repository.exception.RecordNotFoundException +import org.jooq.DSLContext +import org.jooq.Record +import org.slf4j.LoggerFactory +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.stereotype.Repository +import javax.validation.Validator + +@Repository +class UserRepositoryImpl( + private val dsl: DSLContext, + private val validator: Validator +) : UserRepository { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(UserRepositoryImpl::class.java) + + override fun findOne(id: Long) = + dsl.selectFrom(USER) + .where(USER.ID.eq(id)) + .fetchOne(mapper()) ?: throw RecordNotFoundException() + + override fun findOneWithStats(id: Long) = + dsl.select() + .from(USER) + .join(USER_STATS) + .on(USER.ID.eq(USER_STATS.USER_ID)) + .where(USER.ID.eq(id)) + .fetchOne(mapperWithStats()) ?: throw RecordNotFoundException() + + override fun findOneByUsername(username: String): User? { + return dsl.selectFrom(USER) + .where(USER.USERNAME.eq(username)) + .fetchOne(mapper()) + } + + override fun findAll(page: Int, size: Int): Page { + val content = dsl.selectFrom(USER) + .orderBy(USER.ID) + .seek((page - 1) * size.toLong()) + .limit(size) + .fetch(mapper()) + + val totalPages: Int = dsl.selectCount() + .from(USER) + .fetchOne() + .value1() + .div(size.toDouble()) + .ceil() + .toInt() + + return PageImpl( + content = content, + totalPages = totalPages + ) + } + + override fun create(user: User): User { + validate(user) + + try { + return dsl.insertInto(USER, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(user.username, user.password, user.name) + .returning() + .fetchOne() + .let(mapper()) + } catch (e: DataIntegrityViolationException) { + throw EmailDuplicatedException("") + } + } + + override fun update(user: User) { + validate(user) + + try { + dsl.update(USER) + .set(USER.USERNAME, user.username) + .set(USER.PASSWORD, user.password) + .set(USER.NAME, user.name) + .where(USER.ID.eq(user.id)) + .execute() + } catch(e: DataIntegrityViolationException) { + throw EmailDuplicatedException("") + } + } + + private fun validate(user: User) = validator.validate(user).apply { + if (isNotEmpty()) throw RecordInvalidException(toString()) + } + + private fun mapper(): (UserRecord) -> User = ::User + + private fun mapperWithStats(): (Record) -> User = { + mapper().invoke(it.into(USER)).copy( + userStats = UserStats(it.into(USER_STATS)) + ) + } + + private fun Double.ceil() = Math.ceil(this) +} diff --git a/src/main/kotlin/com/myapp/repository/exception/EmailDuplicatedException.kt b/src/main/kotlin/com/myapp/repository/exception/EmailDuplicatedException.kt new file mode 100644 index 0000000..b99f281 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/exception/EmailDuplicatedException.kt @@ -0,0 +1,5 @@ +package com.myapp.repository.exception + +import org.springframework.dao.DuplicateKeyException + +class EmailDuplicatedException(msg: String) : DuplicateKeyException(msg) diff --git a/src/main/kotlin/com/myapp/repository/exception/RecordInvalidException.kt b/src/main/kotlin/com/myapp/repository/exception/RecordInvalidException.kt new file mode 100644 index 0000000..a68358c --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/exception/RecordInvalidException.kt @@ -0,0 +1,5 @@ +package com.myapp.repository.exception + +import org.springframework.dao.DataIntegrityViolationException + +class RecordInvalidException(msg: String) : DataIntegrityViolationException(msg) \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/exception/RecordNotFoundException.kt b/src/main/kotlin/com/myapp/repository/exception/RecordNotFoundException.kt new file mode 100644 index 0000000..dcf0340 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/exception/RecordNotFoundException.kt @@ -0,0 +1,3 @@ +package com.myapp.repository.exception + +class RecordNotFoundException : RuntimeException() \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/repository/exception/RelationshipDuplicatedException.kt b/src/main/kotlin/com/myapp/repository/exception/RelationshipDuplicatedException.kt new file mode 100644 index 0000000..eaeea41 --- /dev/null +++ b/src/main/kotlin/com/myapp/repository/exception/RelationshipDuplicatedException.kt @@ -0,0 +1,5 @@ +package com.myapp.repository.exception + +import org.springframework.dao.DuplicateKeyException + +class RelationshipDuplicatedException(msg: String) : DuplicateKeyException(msg) diff --git a/src/main/kotlin/com/myapp/service/FeedService.kt b/src/main/kotlin/com/myapp/service/FeedService.kt new file mode 100644 index 0000000..d558438 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/FeedService.kt @@ -0,0 +1,10 @@ +package com.myapp.service + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams + +interface FeedService { + + fun findFeed(pageParams: PageParams): List + +} diff --git a/src/main/kotlin/com/myapp/service/FeedServiceImpl.kt b/src/main/kotlin/com/myapp/service/FeedServiceImpl.kt new file mode 100644 index 0000000..3b07b88 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/FeedServiceImpl.kt @@ -0,0 +1,24 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.repository.FeedRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class FeedServiceImpl( + private val feedRepository: FeedRepository, + override val securityContextService: SecurityContextService +) : FeedService, WithCurrentUser { + + override fun findFeed(pageParams: PageParams): List { + val currentUser = currentUserOrThrow() + + return feedRepository.findFeed(currentUser.id, pageParams) + .map { it.copy(isMyPost = it.user.id == currentUser.id) } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/MicropostService.kt b/src/main/kotlin/com/myapp/service/MicropostService.kt new file mode 100644 index 0000000..4467ee1 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/MicropostService.kt @@ -0,0 +1,12 @@ +package com.myapp.service + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams + + +interface MicropostService { + fun create(content: String): Micropost + fun delete(id: Long) + fun findAllByUser(userId: Long, pageParams: PageParams): List + fun findMyPosts(pageParams: PageParams): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/MicropostServiceImpl.kt b/src/main/kotlin/com/myapp/service/MicropostServiceImpl.kt new file mode 100644 index 0000000..be7b7a4 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/MicropostServiceImpl.kt @@ -0,0 +1,48 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.repository.MicropostRepository +import com.myapp.service.exception.NotAuthorizedException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Service +@Transactional +class MicropostServiceImpl( + private val micropostRepository: MicropostRepository, + override val securityContextService: SecurityContextService +) : MicropostService, WithCurrentUser { + + override fun findAllByUser(userId: Long, pageParams: PageParams): List { + val currentUser = currentUser() + val isMyPost = currentUser?.let { it.id == userId } + + return micropostRepository.findAllByUser(userId, pageParams) + .map { it.copy(isMyPost = isMyPost) } + } + + override fun findMyPosts(pageParams: PageParams): List { + val currentUser = currentUserOrThrow() + return findAllByUser(currentUser.id, pageParams) + } + + override fun create(content: String) = + micropostRepository.create(Micropost( + content = content, + user = currentUserOrThrow() + )) + + override fun delete(id: Long) { + val currentUser = currentUserOrThrow() + val post = micropostRepository.findOne(id) + + if (post.user.id == currentUser.id) + micropostRepository.delete(id) + else + throw NotAuthorizedException("You can not delete this post.") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/RelatedUserService.kt b/src/main/kotlin/com/myapp/service/RelatedUserService.kt new file mode 100644 index 0000000..e2491e0 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/RelatedUserService.kt @@ -0,0 +1,9 @@ +package com.myapp.service + +import com.myapp.domain.RelatedUser +import com.myapp.dto.request.PageParams + +interface RelatedUserService { + fun findFollowings(userId: Long, pageParams: PageParams): List + fun findFollowers(userId: Long, pageParams: PageParams): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/RelatedUserServiceImpl.kt b/src/main/kotlin/com/myapp/service/RelatedUserServiceImpl.kt new file mode 100644 index 0000000..5fc0706 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/RelatedUserServiceImpl.kt @@ -0,0 +1,48 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.RelatedUser +import com.myapp.dto.request.PageParams +import com.myapp.repository.RelatedUserRepository +import com.myapp.repository.RelationshipRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Service +@Transactional +class RelatedUserServiceImpl( + private val relatedUserRepository: RelatedUserRepository, + private val relationshipRepository: RelationshipRepository, + override val securityContextService: SecurityContextService +) : RelatedUserService, WithCurrentUser { + + override fun findFollowers(userId: Long, pageParams: PageParams): List { + val relatedUsers = relatedUserRepository.findFollowers(userId, pageParams) + val myRelationships = currentUser()?.let { + val relatedUserIds = relatedUsers.map { it.id }.distinct() + relationshipRepository.findAllByFollowerAndFollowedUsers(it.id, relatedUserIds) + } + + return relatedUsers.map { relatedUser -> + relatedUser.copy( + isFollowedByMe = myRelationships?.any { it.followed.id == relatedUser.id } + ) + } + } + + override fun findFollowings(userId: Long, pageParams: PageParams): List { + val relatedUsers = relatedUserRepository.findFollowings(userId, pageParams) + val myRelationships = currentUser()?.let { + val relatedUserIds = relatedUsers.map { it.id }.distinct() + relationshipRepository.findAllByFollowerAndFollowedUsers(it.id, relatedUserIds) + } + + return relatedUsers.map { relatedUser -> + relatedUser.copy( + isFollowedByMe = myRelationships?.any { it.followed.id == relatedUser.id } + ) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/RelationshipService.kt b/src/main/kotlin/com/myapp/service/RelationshipService.kt new file mode 100644 index 0000000..2d797c3 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/RelationshipService.kt @@ -0,0 +1,6 @@ +package com.myapp.service + +interface RelationshipService { + fun follow(userId: Long) + fun unfollow(userId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/RelationshipServiceImpl.kt b/src/main/kotlin/com/myapp/service/RelationshipServiceImpl.kt new file mode 100644 index 0000000..86ad057 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/RelationshipServiceImpl.kt @@ -0,0 +1,44 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Relationship +import com.myapp.repository.RelationshipRepository +import com.myapp.repository.UserRepository +import com.myapp.service.exception.RelationshipNotFoundException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Service +@Transactional +class RelationshipServiceImpl( + private val userRepository: UserRepository, + private val relationshipRepository: RelationshipRepository, + override val securityContextService: SecurityContextService +) : RelationshipService, WithCurrentUser { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(RelationshipServiceImpl::class.java) + + override fun follow(userId: Long) { + val user = userRepository.findOne(userId) + val currentUser = currentUserOrThrow() + val relationship = Relationship( + follower = currentUser, + followed = user + ) + relationshipRepository.create(relationship) + } + + override fun unfollow(userId: Long) { + val currentUser = currentUserOrThrow() + val relationship = relationshipRepository + .findOneByFollowerAndFollowed(currentUser.id, userId) + + relationship?.let { + relationshipRepository.delete(it.id) + } ?: throw RelationshipNotFoundException() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/UserDetailsServiceImpl.kt b/src/main/kotlin/com/myapp/service/UserDetailsServiceImpl.kt new file mode 100644 index 0000000..3710428 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/UserDetailsServiceImpl.kt @@ -0,0 +1,25 @@ +package com.myapp.service + +import com.myapp.domain.UserDetailsImpl +import com.myapp.repository.UserRepository +import org.springframework.security.authentication.AccountStatusUserDetailsChecker +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service + + +@Service +class UserDetailsServiceImpl( + private val userRepository: UserRepository +) : UserDetailsService { + + override fun loadUserByUsername(username: String): UserDetailsImpl = + userRepository + .findOneByUsername(username) + ?.let(::UserDetailsImpl) + ?.apply { + AccountStatusUserDetailsChecker().check(this) + } ?: throw UsernameNotFoundException("user not found.") + +} + diff --git a/src/main/kotlin/com/myapp/service/UserService.kt b/src/main/kotlin/com/myapp/service/UserService.kt new file mode 100644 index 0000000..924158c --- /dev/null +++ b/src/main/kotlin/com/myapp/service/UserService.kt @@ -0,0 +1,14 @@ +package com.myapp.service + +import com.myapp.domain.User +import com.myapp.dto.page.Page +import com.myapp.dto.request.UserEditParams +import com.myapp.dto.request.UserNewParams + +interface UserService { + fun findAll(page: Int, size: Int = 20): Page + fun findOne(id: Long): User + fun findMe(): User + fun create(params: UserNewParams): User + fun updateMe(params: UserEditParams) +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/UserServiceImpl.kt b/src/main/kotlin/com/myapp/service/UserServiceImpl.kt new file mode 100644 index 0000000..ba2725f --- /dev/null +++ b/src/main/kotlin/com/myapp/service/UserServiceImpl.kt @@ -0,0 +1,62 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.User +import com.myapp.dto.page.Page +import com.myapp.dto.request.UserEditParams +import com.myapp.dto.request.UserNewParams +import com.myapp.repository.RelationshipRepository +import com.myapp.repository.UserRepository +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class UserServiceImpl( + private val userRepository: UserRepository, + private val relationshipRepository: RelationshipRepository, + override val securityContextService: SecurityContextService +) : UserService, WithCurrentUser { + + override fun findOne(id: Long): User { + val currentUser = currentUser() + val user = userRepository.findOneWithStats(id) + val isFollowedByMe = currentUser?.let { + relationshipRepository.findOneByFollowerAndFollowed(it.id, user.id) != null + } + val isMyself = currentUser?.let { it.id == user.id } + + return user.copy( + isFollowedByMe = isFollowedByMe, + isMyself = isMyself + ) + } + + override fun findMe(): User { + val currentUser = currentUserOrThrow() + return findOne(currentUser.id) + } + + override fun findAll(page: Int, size: Int): Page = + userRepository.findAll(page, size) + + override fun create(params: UserNewParams): User { + return userRepository.create(User( + username = params.email, + password = encrypt(params.password), + name = params.name + )) + } + + override fun updateMe(params: UserEditParams) { + val currentUser = currentUserOrThrow() + userRepository.update(currentUser.copy( + username = params.email ?: currentUser.username, + password = params.password?.let { encrypt(it) } ?: currentUser.password, + name = params.name ?: currentUser.name + )) + } + + private fun encrypt(secret: String) = BCryptPasswordEncoder().encode(secret) +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/WithCurrentUser.kt b/src/main/kotlin/com/myapp/service/WithCurrentUser.kt new file mode 100644 index 0000000..d699f6b --- /dev/null +++ b/src/main/kotlin/com/myapp/service/WithCurrentUser.kt @@ -0,0 +1,16 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.User +import org.springframework.security.access.AccessDeniedException + +interface WithCurrentUser { + + val securityContextService: SecurityContextService + + fun currentUser(): User? = + securityContextService.currentUser() + + fun currentUserOrThrow(): User = + securityContextService.currentUser() ?: throw AccessDeniedException("") +} \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/exception/NotAuthorizedException.kt b/src/main/kotlin/com/myapp/service/exception/NotAuthorizedException.kt new file mode 100644 index 0000000..948901d --- /dev/null +++ b/src/main/kotlin/com/myapp/service/exception/NotAuthorizedException.kt @@ -0,0 +1,3 @@ +package com.myapp.service.exception + +class NotAuthorizedException(msg: String) : RuntimeException(msg) \ No newline at end of file diff --git a/src/main/kotlin/com/myapp/service/exception/RelationshipNotFoundException.kt b/src/main/kotlin/com/myapp/service/exception/RelationshipNotFoundException.kt new file mode 100644 index 0000000..564e642 --- /dev/null +++ b/src/main/kotlin/com/myapp/service/exception/RelationshipNotFoundException.kt @@ -0,0 +1,3 @@ +package com.myapp.service.exception + +class RelationshipNotFoundException : RuntimeException() \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1b7bedd..051579e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,73 +3,51 @@ spring: banner-mode: 'OFF' profiles: active: dev + jooq: + sql-dialect: mysql security: basic: enabled: false -endpoints: - enabled: false - health.enabled: true management: context-path: /manage + app.jwt.secret: qwerty --- spring: profiles: dev datasource: - driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy - url: jdbc:log4jdbc:h2:./db/dev;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE + driverClassName: org.h2.Driver + url: jdbc:h2:./db/dev;MODE=MySQL username: sa password: - jpa: - database: H2 - hibernate: - ddl-auto: update - thymeleaf: - cache: false -flyway: - enabled: false --- spring: profiles: dev2 datasource: + type: com.zaxxer.hikari.HikariDataSource driverClassName: org.mariadb.jdbc.Driver - jdbcUrl: jdbc:mysql://localhost:3306/springboot-angular2-tutorial?useSSL=false + url: jdbc:mysql://localhost:3306/springboot-angular2-tutorial username: root password: - jpa: - database: MYSQL - hibernate: - ddl-auto: validate logging: config: classpath:logback-prod.xml --- spring: profiles: test datasource: - driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy - url: jdbc:log4jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE + driverClassName: org.h2.Driver + url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE username: sa password: - jpa: - database: H2 - hibernate: - ddl-auto: create -flyway: - enabled: false -server: - port: 0 --- spring: profiles: stg datasource: + type: com.zaxxer.hikari.HikariDataSource driverClassName: org.mariadb.jdbc.Driver - jdbcUrl: jdbc:mysql://${MYSQL_ENDPOINT}/ebdb?useSSL=false + url: jdbc:mysql://${MYSQL_ENDPOINT}/ebdb?useSSL=false username: micropostuser password: ENC(jSxIbUwZQ5KgQJkOLGbYsZV83hH3oI0D) - jpa: - database: MYSQL - hibernate: - ddl-auto: validate logging: config: classpath:logback-prod.xml app.jwt.secret: ENC(L4Y4hVbkFgZC3VFVWH28jTiCSSCBAx6xeM/nBSIahjrbc/JggbhTiKr2w9RLu9sx) @@ -77,14 +55,12 @@ app.jwt.secret: ENC(L4Y4hVbkFgZC3VFVWH28jTiCSSCBAx6xeM/nBSIahjrbc/JggbhTiKr2w9RL spring: profiles: prod datasource: + type: com.zaxxer.hikari.HikariDataSource driverClassName: org.mariadb.jdbc.Driver - jdbcUrl: jdbc:mysql://${MYSQL_ENDPOINT}/ebdb?useSSL=false + url: jdbc:mysql://${MYSQL_ENDPOINT}/ebdb?useSSL=false username: micropostuser password: ENC(eKw2P1jyrqKKs6KVg9Ql9iaJC8ve2kD8) - jpa: - database: MYSQL - hibernate: - ddl-auto: validate logging: config: classpath:logback-prod.xml app.jwt.secret: ENC(t0NojUi8WhsSCti12Qj8CgzzrBA1Wt0PC9R1N5KvRltEcegGhxSy1zEmI7OxqzOA) + diff --git a/src/main/resources/db/migration/V1__create-schema.sql b/src/main/resources/db/migration/V1__create-schema.sql deleted file mode 100644 index b5a267f..0000000 --- a/src/main/resources/db/migration/V1__create-schema.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE TABLE `user` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT, - `name` VARCHAR(30) NOT NULL, - `password` VARCHAR(100) NOT NULL, - `username` VARCHAR(30) NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `UK_username` (`username`) -) - ENGINE = InnoDB - DEFAULT CHARSET = utf8; - -CREATE TABLE `micropost` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT, - `content` VARCHAR(255) NOT NULL, - `created_at` DATETIME NOT NULL, - `user_id` BIGINT(20) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_user` (`user_id`), - CONSTRAINT `FK_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) - ENGINE = InnoDB - DEFAULT CHARSET = utf8; - -CREATE TABLE `relationship` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT, - `followed_id` BIGINT(20) NOT NULL, - `follower_id` BIGINT(20) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK_followed` (`followed_id`), - KEY `FK_follower` (`follower_id`), - CONSTRAINT `FK_follower` FOREIGN KEY (`follower_id`) REFERENCES `user` (`id`), - CONSTRAINT `FK_followed` FOREIGN KEY (`followed_id`) REFERENCES `user` (`id`), - UNIQUE KEY `UK_follower_followed` (`follower_id`, `followed_id`) -) - ENGINE = InnoDB - DEFAULT CHARSET = utf8; - diff --git a/src/main/resources/log4jdbc.log4j2.properties b/src/main/resources/log4jdbc.log4j2.properties deleted file mode 100644 index 37e0d34..0000000 --- a/src/main/resources/log4jdbc.log4j2.properties +++ /dev/null @@ -1,2 +0,0 @@ -log4jdbc.dump.sql.maxlinelength=0 -log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1c9e046..95ed62a 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,8 +1,6 @@ - - - - + + \ No newline at end of file diff --git a/src/test/groovy/com/myapp/auth/TestSecurityConfig.groovy b/src/test/groovy/com/myapp/auth/TestSecurityConfig.groovy deleted file mode 100644 index 155d0a2..0000000 --- a/src/test/groovy/com/myapp/auth/TestSecurityConfig.groovy +++ /dev/null @@ -1,29 +0,0 @@ -package com.myapp.auth - -import com.myapp.repository.UserRepository -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.security.core.userdetails.UserDetailsService -import spock.mock.DetachedMockFactory - -@SuppressWarnings("GroovyUnusedDeclaration") -@TestConfiguration -class TestSecurityConfig { - - private DetachedMockFactory factory = new DetachedMockFactory() - - @Bean - UserDetailsService userDetailsService() { - factory.Stub(UserDetailsService) - } - - @Bean - TokenAuthenticationService tokenAuthenticationService() { - factory.Stub(TokenAuthenticationService) - } - - @Bean - UserRepository userRepository() { - factory.Stub(UserRepository) - } -} diff --git a/src/test/groovy/com/myapp/controller/AuthControllerTest.groovy b/src/test/groovy/com/myapp/controller/AuthControllerTest.groovy deleted file mode 100644 index 11e4e16..0000000 --- a/src/test/groovy/com/myapp/controller/AuthControllerTest.groovy +++ /dev/null @@ -1,93 +0,0 @@ -package com.myapp.controller - -import com.myapp.auth.TokenHandler -import com.myapp.auth.UserAuthentication -import com.myapp.domain.User -import com.myapp.service.SecurityContextService -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.http.MediaType -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.authentication.BadCredentialsException -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import spock.mock.DetachedMockFactory - -import static groovy.json.JsonOutput.toJson -import static org.hamcrest.Matchers.is -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(AuthController) -class AuthControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - AuthenticationManager authenticationManager(DetachedMockFactory f) { - return f.Mock(AuthenticationManager) - } - - @Bean - TokenHandler tokenHandler(DetachedMockFactory f) { - return f.Mock(TokenHandler) - } - - @Bean - SecurityContextService securityContextService(DetachedMockFactory f) { - return f.Mock(SecurityContextService) - } - } - - @Autowired - AuthenticationManager authenticationManager - - @Autowired - TokenHandler tokenHandler - - @Autowired - SecurityContextService securityContextService - - def setup() { - User user = new User(id: 1, username: "test1@test.com", password: "secret", name: "test1") - UsernamePasswordAuthenticationToken loginToken = new UsernamePasswordAuthenticationToken("test1@test.com", "secret"); - authenticationManager.authenticate(loginToken) >> new UserAuthentication(user) - authenticationManager.authenticate(_ as Authentication) >> { - throw new BadCredentialsException("") - } - securityContextService.currentUser() >> Optional.of(user) - tokenHandler.createTokenForUser(user) >> "created jwt" - } - - def "can auth when username and password are correct"() { - when: - def response = perform(post("/api/auth") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: "test1@test.com", password: "secret")) - ) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$.token', is("created jwt"))) - } - } - - def "can not auth when username or password is not correct"() { - when: - def response = perform(post("/api/auth") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: "test2@test.com", password: "secret")) - ) - - then: - with(response) { - andExpect(status().isUnauthorized()) - } - } - -} - diff --git a/src/test/groovy/com/myapp/controller/BaseControllerTest.groovy b/src/test/groovy/com/myapp/controller/BaseControllerTest.groovy deleted file mode 100644 index b2d756d..0000000 --- a/src/test/groovy/com/myapp/controller/BaseControllerTest.groovy +++ /dev/null @@ -1,60 +0,0 @@ -package com.myapp.controller - -import com.myapp.auth.TokenAuthenticationService -import com.myapp.auth.UserAuthentication -import com.myapp.domain.User -import com.myapp.service.RelationshipService -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan -import org.springframework.security.core.Authentication -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.RequestBuilder -import org.springframework.test.web.servlet.ResultActions -import spock.lang.Specification -import spock.mock.DetachedMockFactory - -import javax.servlet.http.HttpServletRequest - -@ActiveProfiles("test") -@ComponentScan(basePackages = ["com.myapp.auth"]) -abstract class BaseControllerTest extends Specification { - - @SuppressWarnings("GroovyUnusedDeclaration") - final Logger logger = LoggerFactory.getLogger(this.getClass()); - - @TestConfiguration - static class Config { - @Bean - DetachedMockFactory detachedMockFactory() { - return new DetachedMockFactory() - } - } - - @Autowired - private MockMvc mockMvc - - @Autowired - private TokenAuthenticationService tokenAuthenticationService - - ResultActions perform(RequestBuilder requestBuilder) { - return mockMvc.perform(requestBuilder) - } - - User signIn(User user) { - Authentication auth = new UserAuthentication(user) - tokenAuthenticationService.getAuthentication(_ as HttpServletRequest) >> auth - return user - } - - User signIn() { - User user = new User(id: 1, username: "test@test.com", password: "secret", name: "test") - Authentication auth = new UserAuthentication(user) - tokenAuthenticationService.getAuthentication(_ as HttpServletRequest) >> auth - return user - } -} diff --git a/src/test/groovy/com/myapp/controller/FeedControllerTest.groovy b/src/test/groovy/com/myapp/controller/FeedControllerTest.groovy deleted file mode 100644 index 9ba4c1f..0000000 --- a/src/test/groovy/com/myapp/controller/FeedControllerTest.groovy +++ /dev/null @@ -1,66 +0,0 @@ -package com.myapp.controller - -import com.myapp.domain.Micropost -import com.myapp.domain.User -import com.myapp.dto.PageParams -import com.myapp.dto.PostDTO -import com.myapp.service.MicropostService -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import spock.mock.DetachedMockFactory - -import java.time.LocalDateTime - -import static org.hamcrest.Matchers.* -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(FeedController) -class FeedControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - MicropostService micropostService(DetachedMockFactory f) { - f.Stub(MicropostService, name: "micropostService") - } - } - - @Autowired - MicropostService micropostService - - def "can show feed when signed in"() { - given: - User user = signIn() - Date now = new Date() - - def feed = [PostDTO.newInstance(new Micropost(id: 1, user: user, content: "content1", createdAt: now), true)] - micropostService.findAsFeed(_ as PageParams) >> feed - - when: - def response = perform(get("/api/feed")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$', hasSize(1))) - andExpect(jsonPath('$[0].content', is("content1"))) - andExpect(jsonPath('$[0].isMyPost', is(true))) - andExpect(jsonPath('$[0].createdAt', greaterThanOrEqualTo(now.time))) - andExpect(jsonPath('$[0].user.email', nullValue())) - andExpect(jsonPath('$[0].user.avatarHash', is("b642b4217b34b1e8d3bd915fc65c4452"))) - } - } - - def "can not show feed when not signed in"() { - when: - def response = perform(get("/api/feed")) - - then: - response.andExpect(status().isUnauthorized()) - } - -} diff --git a/src/test/groovy/com/myapp/controller/MicropostControllerTest.groovy b/src/test/groovy/com/myapp/controller/MicropostControllerTest.groovy deleted file mode 100644 index cc42e06..0000000 --- a/src/test/groovy/com/myapp/controller/MicropostControllerTest.groovy +++ /dev/null @@ -1,97 +0,0 @@ -package com.myapp.controller - -import com.myapp.domain.Micropost -import com.myapp.service.MicropostService -import com.myapp.service.exceptions.NotPermittedException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.http.MediaType -import spock.mock.DetachedMockFactory - -import static groovy.json.JsonOutput.toJson -import static org.hamcrest.Matchers.is -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(MicropostController) -class MicropostControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - MicropostService micropostService(DetachedMockFactory f) { - f.Mock(MicropostService, name: "micropostService") - } - } - - @Autowired - MicropostService micropostService - - def "can create a micropost"() { - given: - signIn() - String content = "my content" - - when: - def response = perform(post("/api/microposts") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(content: content)) - ) - - then: - micropostService.saveMyPost(_ as Micropost) >> new Micropost(content) - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$.content').exists()) - andExpect(jsonPath('$.content', is(content))) - } - } - - def "can not create a micropost when not signed in"() { - when: - def response = perform(post("/api/microposts") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(content: "test")) - ) - - then: - response.andExpect(status().isUnauthorized()) - } - - def "can delete a micropost"() { - given: - signIn() - - when: - def response = perform(delete("/api/microposts/1")) - - then: - 1 * micropostService.delete(1) - response.andExpect(status().isOk()) - } - - def "can not delete a micropost when not signed in"() { - when: - def response = perform(delete("/api/microposts/1")) - - then: - response.andExpect(status().isUnauthorized()) - } - - def "can not delete a micropost when have no permission"() { - given: - signIn() - micropostService.delete(1) >> { throw new NotPermittedException("") } - - when: - def response = perform(delete("/api/microposts/1")) - - then: - response.andExpect(status().isForbidden()) - } - -} diff --git a/src/test/groovy/com/myapp/controller/RelationshipControllerTest.groovy b/src/test/groovy/com/myapp/controller/RelationshipControllerTest.groovy deleted file mode 100644 index abcfb71..0000000 --- a/src/test/groovy/com/myapp/controller/RelationshipControllerTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package com.myapp.controller - -import com.myapp.service.RelationshipService -import com.myapp.service.exceptions.RelationshipNotFoundException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import spock.mock.DetachedMockFactory - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(RelationshipController) -class RelationshipControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - RelationshipService relationshipService(DetachedMockFactory f) { - return f.Mock(RelationshipService) - } - } - - @Autowired - RelationshipService relationshipService - - def "can follow another user"() { - given: - signIn() - - when: - def response = perform(post("/api/relationships/to/1")) - - then: - 1 * relationshipService.follow(1) - response.andExpect(status().isOk()) - } - - def "can not follow another user when not signed in"() { - when: - def response = perform(post("/api/relationships/to/1")) - - then: - response.andExpect(status().isUnauthorized()) - } - - def "can unfollow another user"() { - given: - signIn() - - when: - def response = perform(delete("/api/relationships/to/1")) - - then: - 1 * relationshipService.unfollow(1) - response.andExpect(status().isOk()) - } - - def "can not unfollow another user when not signed in"() { - when: - def response = perform(delete("/api/relationships/to/1")) - - then: - response.andExpect(status().isUnauthorized()) - } - - def "can not unfollow another user when have already followed"() { - given: - signIn() - relationshipService.unfollow(1) >> { - throw new RelationshipNotFoundException() - } - - when: - def response = perform(delete("/api/relationships/to/1")) - - then: - response.andExpect(status().isNotFound()) - } - -} diff --git a/src/test/groovy/com/myapp/controller/UserControllerTest.groovy b/src/test/groovy/com/myapp/controller/UserControllerTest.groovy deleted file mode 100644 index 26d2cea..0000000 --- a/src/test/groovy/com/myapp/controller/UserControllerTest.groovy +++ /dev/null @@ -1,259 +0,0 @@ -package com.myapp.controller - -import com.myapp.Utils -import com.myapp.domain.User -import com.myapp.domain.UserStats -import com.myapp.dto.UserDTO -import com.myapp.dto.UserParams -import com.myapp.service.UserService -import com.myapp.service.exceptions.UserNotFoundException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.dao.DataIntegrityViolationException -import org.springframework.data.domain.PageImpl -import org.springframework.data.domain.PageRequest -import org.springframework.http.MediaType -import spock.mock.DetachedMockFactory - -import static groovy.json.JsonOutput.toJson -import static org.hamcrest.Matchers.* -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(UserController) -class UserControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - UserService userService(DetachedMockFactory f) { - return f.Mock(UserService) - } - } - - @Autowired - UserService userService - - def "can signup"() { - given: - def email = "akirasosa@test.com" - def password = "secret123" - def name = "akira" - - when: - def response = perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: email, password: password, name: name)) - ) - - then: - 1 * userService.create(new UserParams(email, password, name)) - response.andExpect(status().isOk()) - } - - def "can not signup when email is duplicated"() { - given: - def email = "akirasosa@test.com" - def password = "secret123" - def name = "akira1" - userService.create(_ as UserParams) >> { - throw new DataIntegrityViolationException("") - } - - when: - def response = perform(post("/api/users") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: email, password: password, name: name)) - ) - - then: - with(response) { - andExpect(status().isBadRequest()) - andExpect(jsonPath('$.code', is("email_already_taken"))) - } - } - - def "can list users when signed in"() { - given: - signIn() - userService.findAll(_ as PageRequest) >> { - List content = (0..1).collect { - User u = new User(id: it, username: "test${it}@test.com", password: "secret", name: "test${it}") - UserDTO.builder() - .id(u.id) - .name(u.name) - .avatarHash(Utils.md5(u.username)) - .build() - } - return new PageImpl<>(content) - } - - when: - def response = perform(get("/api/users")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$.content').exists()) - andExpect(jsonPath('$.content', hasSize(2))) - andExpect(jsonPath('$.content[0].name', is("test0"))) - andExpect(jsonPath('$.content[0].email', nullValue())) - andExpect(jsonPath('$.content[0].avatarHash', is("17c9ea0d5cb514cd00d3a71eb312b9dc"))) - andExpect(jsonPath('$.content[1].name', is("test1"))) - } - } - - def "can not list users when not signed in"() { - given: - userService.findAll(_ as PageRequest) >> [] - - when: - def response = perform(get("/api/users")) - - then: - response.andExpect(status().isUnauthorized()) - } - - def "can show user"() { - given: - userService.findOne(1) >> { - User user = new User(id: 1, username: "test1@test.com", password: "secret", name: "test") - UserStats userStats = new UserStats(3, 2, 1) - UserDTO userDTO = UserDTO.builder() - .id(user.id) - .name(user.name) - .avatarHash(Utils.md5(user.username)) - .userStats(userStats) - .build() - return Optional.of(userDTO) - } - - when: - def response = perform(get("/api/users/1")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$.name', is("test"))) - andExpect(jsonPath('$.email', nullValue())) - andExpect(jsonPath('$.avatarHash', is("94fba03762323f286d7c3ca9e001c541"))) - andExpect(jsonPath('$.isFollowedByMe', nullValue())) - andExpect(jsonPath('$.userStats').exists()) - andExpect(jsonPath('$.userStats.micropostCnt', is(3))) - andExpect(jsonPath('$.userStats.followingCnt', is(2))) - andExpect(jsonPath('$.userStats.followerCnt', is(1))) - } - } - - def "can not show user when user was not found"() { - given: - userService.findOne(1) >> { throw new UserNotFoundException() } - - when: - def response = perform(get("/api/users/1")) - - then: - with(response) { - andExpect(status().isNotFound()) - } - } - - def "can show logged in user when signed in"() { - given: - User user = signIn() - userService.findMe() >> { - UserStats userStats = new UserStats(3, 2, 1) - UserDTO userDTO = UserDTO.builder() - .id(user.id) - .name(user.name) - .email(user.username) - .avatarHash(Utils.md5(user.username)) - .userStats(userStats) - .isFollowedByMe(false) - .build() - Optional.of(userDTO) - } - - when: - def response = perform(get("/api/users/me")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$.name', is("test"))) - andExpect(jsonPath('$.email', is("test@test.com"))) - andExpect(jsonPath('$.avatarHash', is("b642b4217b34b1e8d3bd915fc65c4452"))) - andExpect(jsonPath('$.isFollowedByMe', is(false))) - andExpect(jsonPath('$.userStats').exists()) - andExpect(jsonPath('$.userStats.micropostCnt', is(3))) - andExpect(jsonPath('$.userStats.followingCnt', is(2))) - andExpect(jsonPath('$.userStats.followerCnt', is(1))) - } - } - - def "can not show logged in user when not signed in"() { - when: - def response = perform(get("/api/users/me")) - - then: - with(response) { - andExpect(status().isUnauthorized()) - } - } - - def "can update me"() { - given: - signIn() - String email = "test2@test.com" - String password = "very secret" - String name = "new name" - userService.updateMe(new UserParams(email, password, name)) >> { - return new User(id: 1, username: email, password: password, name: name) - } - - when: - def response = perform(patch("/api/users/me") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: email, password: password, name: name)) - ) - - then: - with(response) { - andExpect(status().isOk()) - } - } - - def "can not update me when not signed in"() { - when: - def response = perform(patch("/api/users/me") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: "test", password: "test", name: "test")) - ) - - then: - with(response) { - andExpect(status().isUnauthorized()) - } - } - - def "can not update me when parameter is invalid"() { - given: - signIn() - - when: - // password is too short - def response = perform(patch("/api/users/me") - .contentType(MediaType.APPLICATION_JSON) - .content(toJson(email: "test@test.com", password: "a", name: "test")) - ) - - then: - with(response) { - andExpect(status().isBadRequest()) - } - } - -} diff --git a/src/test/groovy/com/myapp/controller/UserMicropostControllerTest.groovy b/src/test/groovy/com/myapp/controller/UserMicropostControllerTest.groovy deleted file mode 100644 index a584622..0000000 --- a/src/test/groovy/com/myapp/controller/UserMicropostControllerTest.groovy +++ /dev/null @@ -1,92 +0,0 @@ -package com.myapp.controller - -import com.myapp.domain.Micropost -import com.myapp.domain.User -import com.myapp.dto.PageParams -import com.myapp.dto.PostDTO -import com.myapp.service.MicropostService -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import spock.mock.DetachedMockFactory - -import static org.hamcrest.Matchers.* -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(UserMicropostController) -class UserMicropostControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - MicropostService micropostService(DetachedMockFactory f) { - return f.Mock(MicropostService) - } - } - - @Autowired - MicropostService micropostService - - def "can list microposts"() { - given: - User user = new User(id: 1, username: "akira@test.com", password: "secret", name: "akira") - micropostService.findByUser(1, new PageParams()) >> (0..1).collect { - Micropost post = new Micropost(id: it, content: "content${it}", user: user, createdAt: new Date()) - return PostDTO.newInstance(post, null) - } - - - when: - def response = perform(get("/api/users/${user.id}/microposts/")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$', hasSize(2))) - andExpect(jsonPath('$[0].content', is("content0"))) - andExpect(jsonPath('$[0].isMyPost', nullValue())) - andExpect(jsonPath('$[0].user.name', is("akira"))) - andExpect(jsonPath('$[1].content', is("content1"))) - } - } - - def "can list my microposts when signed in"() { - given: - User user = signIn() - micropostService.findMyPosts(new PageParams()) >> (0..1).collect { - Micropost post = new Micropost(id: it, content: "content${it}", user: user, createdAt: new Date()) - return PostDTO.newInstance(post, true) - } - - - when: - def response = perform(get("/api/users/me/microposts")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$', hasSize(2))) - andExpect(jsonPath('$[0].content', is("content0"))) - andExpect(jsonPath('$[0].isMyPost', is(true))) - andExpect(jsonPath('$[0].user.name', is(user.name))) - andExpect(jsonPath('$[1].content', is("content1"))) - } - } - - def "can not list my microposts when signed in"() { - given: - micropostService.findMyPosts(new PageParams()) >> [] - - when: - def response = perform(get("/api/users/me/microposts")) - - then: - with(response) { - andExpect(status().isUnauthorized()) - } - } - -} diff --git a/src/test/groovy/com/myapp/controller/UserRelationshipControllerTest.groovy b/src/test/groovy/com/myapp/controller/UserRelationshipControllerTest.groovy deleted file mode 100644 index bfae4ae..0000000 --- a/src/test/groovy/com/myapp/controller/UserRelationshipControllerTest.groovy +++ /dev/null @@ -1,95 +0,0 @@ -package com.myapp.controller - -import com.myapp.Utils -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.domain.UserStats -import com.myapp.dto.PageParams -import com.myapp.dto.RelatedUserDTO -import com.myapp.service.RelationshipService -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import spock.mock.DetachedMockFactory - -import static org.hamcrest.Matchers.is -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -@WebMvcTest(UserRelationshipController) -class UserRelationshipControllerTest extends BaseControllerTest { - - @TestConfiguration - static class Config { - @Bean - RelationshipService relationshipService(DetachedMockFactory f) { - return f.Mock(RelationshipService) - } - } - - @Autowired - RelationshipService relationshipService - - def "can list followings"() { - given: - User follower = new User(id: 1, username: "akira@test.com", password: "secret", name: "akira") - User followed1 = new User(id: 2, username: "test1@test.com", password: "secret", name: "test1") - User followed2 = new User(id: 3, username: "test2@test.com", password: "secret", name: "test2") - Relationship r1 = new Relationship(id: 1, follower: follower, followed: followed1) - Relationship r2 = new Relationship(id: 2, follower: follower, followed: followed2) - relationshipService.findFollowings(follower.id, new PageParams()) >> [ - buildRelatedUserDTO(followed1, r1), - buildRelatedUserDTO(followed2, r2), - ] - - when: - def response = perform(get("/api/users/${follower.id}/followings")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$[0].name', is("test1"))) - andExpect(jsonPath('$[0].avatarHash', is("94fba03762323f286d7c3ca9e001c541"))) - andExpect(jsonPath('$[0].relationshipId', is(r1.id.intValue()))) - andExpect(jsonPath('$[1].name', is("test2"))) - } - } - - def "can list followers"() { - given: - User followed = new User(id: 1, username: "akira@test.com", password: "secret", name: "akira") - User follower1 = new User(id: 2, username: "test1@test.com", password: "secret", name: "test1") - User follower2 = new User(id: 3, username: "test2@test.com", password: "secret", name: "test2") - Relationship r1 = new Relationship(id: 1, follower: follower1, followed: followed) - Relationship r2 = new Relationship(id: 2, follower: follower2, followed: followed) - relationshipService.findFollowers(followed.id, new PageParams()) >> [ - buildRelatedUserDTO(follower1, r1), - buildRelatedUserDTO(follower2, r2), - ] - - when: - def response = perform(get("/api/users/${followed.id}/followers")) - - then: - with(response) { - andExpect(status().isOk()) - andExpect(jsonPath('$[0].name', is("test1"))) - andExpect(jsonPath('$[0].avatarHash', is("94fba03762323f286d7c3ca9e001c541"))) - andExpect(jsonPath('$[0].relationshipId', is(r1.id.intValue()))) - andExpect(jsonPath('$[1].name', is("test2"))) - } - } - - private static buildRelatedUserDTO(User u, Relationship r) { - UserStats us = new UserStats(1, 2, 3) - return RelatedUserDTO.builder() - .id(u.id) - .avatarHash(Utils.md5(u.username)) - .name(u.name) - .userStats(us) - .relationshipId(r.id) - .build() - } -} diff --git a/src/test/groovy/com/myapp/repository/BaseRepositoryTest.groovy b/src/test/groovy/com/myapp/repository/BaseRepositoryTest.groovy deleted file mode 100644 index 4cb37b8..0000000 --- a/src/test/groovy/com/myapp/repository/BaseRepositoryTest.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package com.myapp.repository - -import com.myapp.config.QueryDSLConfig -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.context.annotation.Import -import org.springframework.test.context.ActiveProfiles -import spock.lang.Specification - -@ActiveProfiles("test") -@Import(value = [QueryDSLConfig]) -@DataJpaTest -abstract class BaseRepositoryTest extends Specification { -} diff --git a/src/test/groovy/com/myapp/repository/MicropostCustomRepositoryTest.groovy b/src/test/groovy/com/myapp/repository/MicropostCustomRepositoryTest.groovy deleted file mode 100644 index 6a0b8e3..0000000 --- a/src/test/groovy/com/myapp/repository/MicropostCustomRepositoryTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -package com.myapp.repository - -import com.myapp.domain.Micropost -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.dto.PageParams -import org.springframework.beans.factory.annotation.Autowired - -class MicropostCustomRepositoryTest extends BaseRepositoryTest { - - @Autowired - MicropostRepository micropostRepository - - @Autowired - MicropostCustomRepository micropostCustomRepository - - @Autowired - UserRepository userRepository - - @Autowired - RelationshipRepository relationshipRepository - - def "can find feed"() { - given: - User follower = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - User followed = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "test1")) - User another = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "test2")) - relationshipRepository.save(new Relationship(follower: follower, followed: followed)) - [follower, followed, another].each { u -> - micropostRepository.save(new Micropost(content: "test1", user: u)) - micropostRepository.save(new Micropost(content: "test2", user: u)) - } - - when: - List result = micropostCustomRepository.findAsFeed(follower, new PageParams()).collect() - - then: - result.size() == 4 - result.first().micropost.user.id == followed.id - result.last().micropost.user.id == follower.id - } - - def "can find feed by since_id or max_id"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - Micropost post1 = micropostRepository.save(new Micropost(content: "test1", user: user)) - Micropost post2 = micropostRepository.save(new Micropost(content: "test2", user: user)) - Micropost post3 = micropostRepository.save(new Micropost(content: "test3", user: user)) - - when: - List result = micropostCustomRepository.findAsFeed(user, new PageParams(sinceId: post2.id)).collect() - - then: - result.size() == 1 - result.first().micropost == post3 - - when: - result = micropostCustomRepository.findAsFeed(user, new PageParams(maxId: post2.id)).collect() - - then: - result.size() == 1 - result.first().micropost == post1 - } - - def "can find posts by user"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - Micropost post1 = micropostRepository.save(new Micropost(content: "test1", user: user)) - Micropost post2 = micropostRepository.save(new Micropost(content: "test2", user: user)) - Micropost post3 = micropostRepository.save(new Micropost(content: "test3", user: user)) - - when: - List result = micropostCustomRepository.findByUser(user, new PageParams(sinceId: post2.id)).collect() - - then: - result.size() == 1 - result.first().micropost == post3 - - when: - result = micropostCustomRepository.findByUser(user, new PageParams(maxId: post2.id)).collect() - - then: - result.size() == 1 - result.first().micropost == post1 - } - -} diff --git a/src/test/groovy/com/myapp/repository/RelatedUserCustomRepositoryTest.groovy b/src/test/groovy/com/myapp/repository/RelatedUserCustomRepositoryTest.groovy deleted file mode 100644 index 95588f8..0000000 --- a/src/test/groovy/com/myapp/repository/RelatedUserCustomRepositoryTest.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package com.myapp.repository - -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.dto.PageParams -import org.springframework.beans.factory.annotation.Autowired - -class RelatedUserCustomRepositoryTest extends BaseRepositoryTest { - - @Autowired - UserRepository userRepository - - @Autowired - RelatedUserCustomRepository relatedUserCustomRepository - - @Autowired - RelationshipRepository relationshipRepository - - def "findFollowings"() { - given: - User follower = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - User followed1 = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "test1")) - User followed2 = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "test2")) - Relationship r1 = relationshipRepository.save(new Relationship(follower: follower, followed: followed1)) - Relationship r2 = relationshipRepository.save(new Relationship(follower: follower, followed: followed2)) - - when: - List result = relatedUserCustomRepository.findFollowings(follower, new PageParams()) - - then: - result[0].user.username == "test2@test.com" - result[0].relationship.id == r2.id - result[1].user.username == "test1@test.com" - result[1].relationship.id == r1.id - } - - def "findFollowers"() { - given: - User followed = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - User follower1 = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "test1")) - User follower2 = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "test2")) - Relationship r1 = relationshipRepository.save(new Relationship(followed: followed, follower: follower1)) - Relationship r2 = relationshipRepository.save(new Relationship(followed: followed, follower: follower2)) - - when: - List result = relatedUserCustomRepository.findFollowers(followed, new PageParams()) - - then: - result[0].user.username == "test2@test.com" - result[0].relationship.id == r2.id - result[1].user.username == "test1@test.com" - result[1].relationship.id == r1.id - } - -} diff --git a/src/test/groovy/com/myapp/repository/RelationshipRepositoryTest.groovy b/src/test/groovy/com/myapp/repository/RelationshipRepositoryTest.groovy deleted file mode 100644 index ec49ab1..0000000 --- a/src/test/groovy/com/myapp/repository/RelationshipRepositoryTest.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package com.myapp.repository - -import com.myapp.domain.Relationship -import com.myapp.domain.User -import org.springframework.beans.factory.annotation.Autowired - -class RelationshipRepositoryTest extends BaseRepositoryTest { - - @Autowired - RelationshipRepository relationshipRepository - - @Autowired - UserRepository userRepository - - def "can find one by follower and followed"() { - given: - User follower = new User(username: "akira@test.com", password: "secret", name: "akira") - User followed = new User(username: "satoru@test.com", password: "secret", name: "satoru") - userRepository.save([follower, followed]) - Relationship relationship = new Relationship(follower: follower, followed: followed) - - when: - def result = relationshipRepository.findOneByFollowerAndFollowed(follower, followed) - - then: - !result.isPresent() - - when: - relationshipRepository.save(relationship) - result = relationshipRepository.findOneByFollowerAndFollowed(follower, followed) - - then: - result.get() == relationship - } - - def "can find all by follower and followed in"() { - given: - User follower = new User(username: "akira@test.com", password: "secret", name: "akira") - User followed1 = new User(username: "followed1@test.com", password: "secret", name: "satoru") - User followed2 = new User(username: "followed2@test.com", password: "secret", name: "satoru") - userRepository.save([follower, followed1, followed2]) - Relationship relationship1 = new Relationship(follower: follower, followed: followed1) - Relationship relationship2 = new Relationship(follower: follower, followed: followed2) - relationshipRepository.save([relationship1, relationship2]) - - when: - List result = relationshipRepository.findAllByFollowerAndFollowedIn(follower, [followed1]).collect() - - then: - result.size() == 1 - result.first().followed == followed1 - } -} diff --git a/src/test/groovy/com/myapp/repository/RepositoryTestConfig.groovy b/src/test/groovy/com/myapp/repository/RepositoryTestConfig.groovy deleted file mode 100644 index 7dec3b3..0000000 --- a/src/test/groovy/com/myapp/repository/RepositoryTestConfig.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package com.myapp.repository - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration -import org.springframework.context.annotation.Configuration -import org.springframework.data.jpa.repository.config.EnableJpaRepositories - -@Configuration -@EnableJpaRepositories(basePackages = "com.myapp.repository") -@EntityScan("com.myapp.domain") -@EnableAutoConfiguration(exclude = [FlywayAutoConfiguration]) -class RepositoryTestConfig { -} diff --git a/src/test/groovy/com/myapp/repository/UserCustomRepositoryTest.groovy b/src/test/groovy/com/myapp/repository/UserCustomRepositoryTest.groovy deleted file mode 100644 index 0e67de8..0000000 --- a/src/test/groovy/com/myapp/repository/UserCustomRepositoryTest.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package com.myapp.repository - -import com.myapp.domain.User -import org.springframework.beans.factory.annotation.Autowired - -class UserCustomRepositoryTest extends BaseRepositoryTest { - - @Autowired - UserRepository userRepository - - @Autowired - UserCustomRepository userCustomRepository - - def "findOne"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - - when: - UserCustomRepository.Row result = userCustomRepository.findOne(user.id).get() - - then: - result.user == user - result.userStats.followerCnt == 0 - result.userStats.followingCnt == 0 - result.userStats.micropostCnt == 0 - } - -} diff --git a/src/test/groovy/com/myapp/service/BaseServiceTest.groovy b/src/test/groovy/com/myapp/service/BaseServiceTest.groovy deleted file mode 100644 index 0def6da..0000000 --- a/src/test/groovy/com/myapp/service/BaseServiceTest.groovy +++ /dev/null @@ -1,27 +0,0 @@ -package com.myapp.service - -import com.myapp.domain.User -import com.myapp.repository.BaseRepositoryTest - -abstract class BaseServiceTest extends BaseRepositoryTest { - - SecurityContextService securityContextService = Mock(SecurityContextService) - - Optional currentUser - - def setup() { - // default not signed in - currentUser = Optional.empty() - securityContextService.currentUser() >> { currentUser } - } - - def signIn(User user) { - currentUser = Optional.of(user) - } - - def cleanup() { - currentUser = Optional.empty() - } - - -} diff --git a/src/test/groovy/com/myapp/service/MicropostServiceTest.groovy b/src/test/groovy/com/myapp/service/MicropostServiceTest.groovy deleted file mode 100644 index 81b8003..0000000 --- a/src/test/groovy/com/myapp/service/MicropostServiceTest.groovy +++ /dev/null @@ -1,149 +0,0 @@ -package com.myapp.service - -import com.myapp.domain.Micropost -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.dto.PageParams -import com.myapp.dto.PostDTO -import com.myapp.repository.MicropostCustomRepository -import com.myapp.repository.MicropostRepository -import com.myapp.repository.RelationshipRepository -import com.myapp.repository.UserRepository -import com.myapp.service.exceptions.NotPermittedException -import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Shared - -@SuppressWarnings("GroovyPointlessBoolean") -class MicropostServiceTest extends BaseServiceTest { - - @Autowired - MicropostRepository micropostRepository - - @Autowired - MicropostCustomRepository micropostCustomRepository - - @Autowired - UserRepository userRepository - - @Shared - MicropostService micropostService - - @Autowired - RelationshipRepository relationshipRepository - - def setup() { - micropostService = new MicropostServiceImpl(micropostRepository, userRepository, micropostCustomRepository, relationshipRepository, securityContextService) - } - - def "can delete micropost when have a permission"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - Micropost post = micropostRepository.save(new Micropost(user: user, content: "test")) - signIn(user) - - when: - micropostService.delete(post.id) - - then: - micropostRepository.count() == 0 - } - - def "can not delete micropost when have no permission"() { - given: - User currentUser = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "akira")) - User anotherUser = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "akira")) - Micropost post = micropostRepository.save(new Micropost(user: anotherUser, content: "test")) - signIn(currentUser) - - when: - micropostService.delete(post.id) - - then: - thrown(NotPermittedException) - micropostRepository.count() == 1 - } - - def "can find posts as feed"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - micropostRepository.save(new Micropost(user: user, content: "my content")) - signIn(user) - - User followed = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "test1")) - relationshipRepository.save(new Relationship(follower: user, followed: followed)) - micropostRepository.save(new Micropost(user: followed, content: "follower content")) - - when: - List posts = micropostService.findAsFeed(new PageParams()) - - then: - posts.size() == 2 - posts.first().content == 'follower content' - posts.first().isMyPost == false - posts.last().isMyPost == true - } - - def "can find posts by user when not signed in"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - micropostRepository.save(new Micropost(user: user, content: "my content")) - - when: - List posts = micropostService.findByUser(user.id, new PageParams()) - - then: - posts.size() == 1 - posts.first().content == "my content" - posts.first().isMyPost == null - } - - def "can find posts by user when signed in"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - signIn(user) - - when: - micropostRepository.save(new Micropost(user: user, content: "my content")) - List posts = micropostService.findByUser(user.id, new PageParams()) - - then: - posts.first().isMyPost == true - - when: - User anotherUser = userRepository.save(new User(username: "satoru@test.com", password: "secret", name: "satoru")) - micropostRepository.save(new Micropost(user: anotherUser, content: "my content")) - List anotherPosts = micropostService.findByUser(anotherUser.id, new PageParams()) - - then: - anotherPosts.first().isMyPost == false - } - - def "can find my posts"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - signIn(user) - micropostRepository.save(new Micropost(user: user, content: "my content")) - - when: - List posts = micropostService.findMyPosts(new PageParams()) - - then: - posts.size() == 1 - posts.first().content == "my content" - posts.first().isMyPost == true - } - - def "can save my post"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - signIn(user) - Micropost post = new Micropost("test post") - - when: - micropostService.saveMyPost(post) - - then: - micropostRepository.findAll().first() == post - post.user == user - } -} diff --git a/src/test/groovy/com/myapp/service/RelationshipServiceTest.groovy b/src/test/groovy/com/myapp/service/RelationshipServiceTest.groovy deleted file mode 100644 index 1145d8f..0000000 --- a/src/test/groovy/com/myapp/service/RelationshipServiceTest.groovy +++ /dev/null @@ -1,114 +0,0 @@ -package com.myapp.service - -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.dto.PageParams -import com.myapp.repository.RelatedUserCustomRepository -import com.myapp.repository.RelationshipRepository -import com.myapp.repository.UserRepository -import org.springframework.beans.factory.annotation.Autowired - -@SuppressWarnings("GroovyPointlessBoolean") -class RelationshipServiceTest extends BaseServiceTest { - - @Autowired - UserRepository userRepository - - @Autowired - RelatedUserCustomRepository relatedUserCustomRepository - - @Autowired - RelationshipRepository relationshipRepository - - RelationshipService relationshipService - - def setup() { - relationshipService = new RelationshipServiceImpl(relationshipRepository, relatedUserCustomRepository, userRepository, securityContextService) - } - - def "can find followings when not signed in"() { - given: - User follower = userRepository.save(new User(username: "follower@test.com", password: "secret", name: "akira")) - User followed = userRepository.save(new User(username: "followed@test.com", password: "secret", name: "akira")) - relationshipRepository.save(new Relationship(follower: follower, followed: followed)) - - when: - def followings = relationshipService.findFollowings(follower.id, new PageParams()) - - then: - followings.first().isFollowedByMe == null - } - - def "can find followings when signed in"() { - given: - User follower = userRepository.save(new User(username: "follower@test.com", password: "secret", name: "akira")) - User followed1 = userRepository.save(new User(username: "followed1@test.com", password: "secret", name: "followed1")) - User followed2 = userRepository.save(new User(username: "followed2@test.com", password: "secret", name: "followed2")) - relationshipRepository.save(new Relationship(follower: follower, followed: followed1)) - relationshipRepository.save(new Relationship(follower: follower, followed: followed2)) - signIn(followed1) - - when: - def followings = relationshipService.findFollowings(follower.id, new PageParams()) - - then: - followings.first().name == "followed2" - followings.first().isFollowedByMe == false - followings.last().name == "followed1" - followings.last().isFollowedByMe == false - } - - def "can find followers when not signed in"() { - given: - User followed = userRepository.save(new User(username: "followed@test.com", password: "secret", name: "akira")) - User follower = userRepository.save(new User(username: "follower@test.com", password: "secret", name: "akira")) - relationshipRepository.save(new Relationship(follower: follower, followed: followed)) - - when: - def followers = relationshipService.findFollowers(followed.id, new PageParams()) - - then: - followers.first().isFollowedByMe == null - } - - def "can find followers when signed in"() { - given: - User followed = userRepository.save(new User(username: "followed@test.com", password: "secret", name: "akira")) - User follower1 = userRepository.save(new User(username: "follower1@test.com", password: "secret", name: "follower1")) - User follower2 = userRepository.save(new User(username: "follower2@test.com", password: "secret", name: "follower2")) - relationshipRepository.save(new Relationship(follower: follower1, followed: followed)) - relationshipRepository.save(new Relationship(follower: follower2, followed: followed)) - signIn(follower1) - - when: - def followers = relationshipService.findFollowers(followed.id, new PageParams()) - - then: - followers.first().name == "follower2" - followers.first().isFollowedByMe == false - followers.last().name == "follower1" - followers.last().isFollowedByMe == false - } - - def "can follow and unfollow"() { - given: - User currentUser = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "akira")) - User targetUser = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "akira")) - signIn(currentUser) - - when: - relationshipService.follow(targetUser.id) - - then: - def relationShip = relationshipRepository.findAll().first() - relationShip.follower == currentUser - relationShip.followed == targetUser - - when: - relationshipService.unfollow(targetUser.id) - - then: - relationshipRepository.count() == 0 - } - -} diff --git a/src/test/groovy/com/myapp/service/UserServiceTest.groovy b/src/test/groovy/com/myapp/service/UserServiceTest.groovy deleted file mode 100644 index c2e8c2b..0000000 --- a/src/test/groovy/com/myapp/service/UserServiceTest.groovy +++ /dev/null @@ -1,152 +0,0 @@ -package com.myapp.service - -import com.myapp.domain.Relationship -import com.myapp.domain.User -import com.myapp.dto.UserDTO -import com.myapp.dto.UserParams -import com.myapp.repository.RelationshipRepository -import com.myapp.repository.UserCustomRepository -import com.myapp.repository.UserRepository -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.data.domain.Page -import org.springframework.data.domain.PageRequest -import org.springframework.security.core.userdetails.UserDetails -import org.springframework.security.core.userdetails.UsernameNotFoundException - -@SuppressWarnings("GroovyPointlessBoolean") -class UserServiceTest extends BaseServiceTest { - - @Autowired - UserRepository userRepository - - @Autowired - UserCustomRepository userCustomRepository - - @Autowired - RelationshipRepository relationshipRepository - - UserService userService - - def setup() { - userService = new UserServiceImpl(userRepository, userCustomRepository, relationshipRepository, securityContextService) - } - - def "can find a user when not signed in"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - - when: - UserDTO userDTO = userService.findOne(user.id).get() - - then: - userDTO.id == user.id - userDTO.isFollowedByMe == null // not signed in - } - - def "can find a user with isFollowedByMe true when signed in"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - User currentUser = userRepository.save(new User(username: "current@test.com", password: "secret", name: "akira")) - signIn(currentUser) - relationshipRepository.save(new Relationship(follower: currentUser, followed: user)) - - when: - UserDTO userDTO = userService.findOne(user.id).get() - - then: - userDTO.id == user.id - userDTO.isFollowedByMe == true - } - - def "can find me"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - signIn(user) - - when: - UserDTO userDTO = userService.findMe().get() - - then: - userDTO.id == user.id - } - - def "can find paged user list"() { - given: - //noinspection GroovyUnusedAssignment - User user1 = userRepository.save(new User(username: "test1@test.com", password: "secret", name: "akira")) - User user2 = userRepository.save(new User(username: "test2@test.com", password: "secret", name: "akira")) - - when: - PageRequest pageRequest = new PageRequest(1, 1) - Page page = userService.findAll(pageRequest) - - then: - page.content.first().id == user2.id - page.totalElements == 2 - } - - def "can create a user"() { - given: - UserParams params = new UserParams("test1@test.com", "secret", "test1") - - when: - User user = userService.create(params) - - then: - userRepository.count() == 1 - user.username == "test1@test.com" - } - - def "can update a user"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - UserParams params = new UserParams("test2@test.com", "secret2", "test2") - - when: - userService.update(user, params) - - then: - user.username == "test2@test.com" - user.name == "test2" - - when: - params = new UserParams("test3@test.com", null, null) - userService.update(user, params) - - then: - user.username == "test3@test.com" - user.name == "test2" - } - - def "can update me"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - UserParams params = new UserParams("test2@test.com", "secret2", "test2") - signIn(user) - - when: - userService.updateMe(params) - - then: - user.username == "test2@test.com" - user.name == "test2" - } - - def "loadUserByUsername"() { - given: - User user = userRepository.save(new User(username: "akira@test.com", password: "secret", name: "akira")) - - when: - UserDetails userDetails = userService.loadUserByUsername("akira@test.com") - - then: - user.username == userDetails.username - - when: - userService.loadUserByUsername("test1@test.com") - - then: - thrown(UsernameNotFoundException) - } - -} diff --git a/src/test/kotlin/com/myapp/auth/SecurityContextServiceTest.kt b/src/test/kotlin/com/myapp/auth/SecurityContextServiceTest.kt new file mode 100644 index 0000000..e4fcdcd --- /dev/null +++ b/src/test/kotlin/com/myapp/auth/SecurityContextServiceTest.kt @@ -0,0 +1,56 @@ +package com.myapp.auth + +import com.myapp.domain.UserDetailsImpl +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder + +class SecurityContextServiceTest { + + private val securityContextService = SecurityContextServiceImpl() + + lateinit private var securityContext: SecurityContext + + @Before + fun storeContext() { + securityContext = SecurityContextHolder.getContext() + } + + @Test + fun `currentUser should return current user`() { + val userDetails = UserDetailsImpl(TestUser) + SecurityContextHolder.setContext(mock { + on { authentication } doReturn UserAuthentication(userDetails) + }) + + val currentUser = securityContextService.currentUser() + + assertThat(currentUser).isNotNull() + assertThat(currentUser).isEqualTo(TestUser) + } + + @Test + fun `currentUser should return null when not signed in`() { + SecurityContextHolder.setContext(mock { + on { authentication } doReturn mock() + }) + + val currentUser = securityContextService.currentUser() + + assertThat(currentUser).isNull() + } + + @After + fun restoreContext() { + SecurityContextHolder.setContext(securityContext) + } + +} + diff --git a/src/test/kotlin/com/myapp/auth/TokenAuthenticationServiceTest.kt b/src/test/kotlin/com/myapp/auth/TokenAuthenticationServiceTest.kt new file mode 100644 index 0000000..7bb7336 --- /dev/null +++ b/src/test/kotlin/com/myapp/auth/TokenAuthenticationServiceTest.kt @@ -0,0 +1,44 @@ +package com.myapp.auth + +import com.myapp.domain.UserDetailsImpl +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import javax.servlet.http.HttpServletRequest + +class TokenAuthenticationServiceTest { + + private val tokenHandler: TokenHandler = mock() + private val tokenAuthenticationService by lazy { + TokenAuthenticationServiceImpl( + tokenHandler = tokenHandler + ) + } + + @Test + fun `authentication get authentication from authorization header`() { + val mockRequest = mock { + on { getHeader("authorization") } doReturn "Bearer jwt123" + } + val dummyUserDetails = UserDetailsImpl(TestUser) + `when`(tokenHandler.parseUserFromToken("jwt123")).doReturn(dummyUserDetails) + + val authentication = tokenAuthenticationService.authentication(mockRequest) + + assertThat(authentication).isNotNull() + assertThat(authentication?.principal).isEqualTo(dummyUserDetails) + } + + @Test + fun `authentication returns null when no authorization header`() { + val mockRequest = mock() + + val authentication = tokenAuthenticationService.authentication(mockRequest) + + assertThat(authentication).isNull() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/auth/TokenHandlerTest.kt b/src/test/kotlin/com/myapp/auth/TokenHandlerTest.kt new file mode 100644 index 0000000..21f3cb0 --- /dev/null +++ b/src/test/kotlin/com/myapp/auth/TokenHandlerTest.kt @@ -0,0 +1,55 @@ +package com.myapp.auth + +import com.myapp.repository.UserRepository +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import io.jsonwebtoken.Jwts +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import java.util.* + +class TokenHandlerTest { + + private val userRepository: UserRepository = mock() + private val tokenHandler by lazy { + TokenHandlerImpl( + secret = "qwerty", + userRepository = userRepository + ) + } + private val ONE_WEEK = 7 * 24 * 60 * 60 * 1000 + + @Test + fun `parseUserFromToken should parse user from a token`() { + `when`(userRepository.findOne(1)) + .doReturn(TestUser.copy( + username = "test1@test.com" + )) + + // This token does not have expiration and has user id = 1 + val jwt = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIn0.Ew-5gopsJtUKofmP-pwDzWvNBCmCd-VIOMNIPaZXVCfn0AUyWmAce2UJ611X35bWiUOFOfuzcz2g6SK2M4Z9Rg" + val userDetails = tokenHandler.parseUserFromToken(jwt) + + assertThat(userDetails.username).isEqualTo("test1@test.com") + } + + + @Test + fun `createTokenForUser should create a token for user`() { + val beginning = Date() + val token = tokenHandler.createTokenForUser(TestUser.copy(_id = 1)) + val jwt = Jwts.parser() + .setSigningKey("qwerty") + .parseClaimsJws(token) + .body + val ending = Date() + + assertThat(jwt.subject.toLong()).isEqualTo(1) + assertThat(jwt.expiration.time) + // jwt does not have milli secs, so that div(1000).times(1000) is required. + .isGreaterThanOrEqualTo(beginning.time.div(1000).times(1000) + ONE_WEEK) + .isLessThanOrEqualTo(ending.time.div(1000).times(1000) + ONE_WEEK) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/AuthControllerTest.kt b/src/test/kotlin/com/myapp/controller/AuthControllerTest.kt new file mode 100644 index 0000000..41e963b --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/AuthControllerTest.kt @@ -0,0 +1,76 @@ +package com.myapp.controller + +import com.myapp.auth.TokenHandler +import com.myapp.domain.UserDetailsImpl +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import org.hamcrest.CoreMatchers.`is` +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(AuthController::class) +class AuthControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var tokenHandler: TokenHandler + + @Autowired + lateinit private var userDetailsService: UserDetailsService + + @Test + fun `auth should auth`() { + val userInDb = TestUser.copy( + username = "test1@test.com", + password = BCryptPasswordEncoder().encode("secret123") + ) + `when`(userDetailsService.loadUserByUsername("test1@test.com")) + .doReturn(UserDetailsImpl(userInDb)) + `when`(tokenHandler.createTokenForUser(userInDb)) + .doReturn("dummy token") + + perform(post("/api/auth") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "secret123" + ) + }) + ).apply { + andExpect(status().isOk) + andExpect(jsonPath("$.token", `is`("dummy token"))) + } + } + + @Test + fun `auth should not auth when email and password is not valid`() { + val userInDb = TestUser.copy( + username = "test1@test.com", + password = BCryptPasswordEncoder().encode("secret123") + ) + `when`(userDetailsService.loadUserByUsername("test1@test.com")) + .doReturn(UserDetailsImpl(userInDb)) + + perform(post("/api/auth") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test2@test.com", + "password" to "secret123" + ) + }) + ).apply { + andExpect(status().isUnauthorized) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/BaseControllerTest.kt b/src/test/kotlin/com/myapp/controller/BaseControllerTest.kt new file mode 100644 index 0000000..e37326a --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/BaseControllerTest.kt @@ -0,0 +1,58 @@ +package com.myapp.controller + +import com.beust.klaxon.JSON +import com.beust.klaxon.JsonObject +import com.beust.klaxon.json +import com.myapp.auth.TokenAuthenticationService +import com.myapp.auth.UserAuthentication +import com.myapp.domain.User +import com.myapp.domain.UserDetailsImpl +import com.myapp.repository.UserRepository +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.context.annotation.ComponentScan +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.RequestBuilder +import org.springframework.test.web.servlet.ResultActions + +@RunWith(SpringRunner::class) +@ActiveProfiles("test") +@ComponentScan(basePackages = arrayOf("com.myapp.auth")) +abstract class BaseControllerTest { + + @Autowired + lateinit private var mvc: MockMvc + + @MockBean + lateinit private var tokenAuthenticationService: TokenAuthenticationService + + @MockBean + lateinit private var userDetailsService: UserDetailsService + + @MockBean + lateinit private var userRepository: UserRepository + + fun signIn(): User { + `when`(tokenAuthenticationService.authentication(any())) + .doReturn(UserAuthentication(UserDetailsImpl(TestUser))) + + return TestUser + } + + fun perform(requestBuilder: RequestBuilder): ResultActions { + return mvc.perform(requestBuilder) + } + + fun jsonString(init: JSON.() -> JsonObject): String { + return json(init).toJsonString() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/FeedControllerTest.kt b/src/test/kotlin/com/myapp/controller/FeedControllerTest.kt new file mode 100644 index 0000000..41d0be6 --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/FeedControllerTest.kt @@ -0,0 +1,53 @@ +package com.myapp.controller + +import com.myapp.service.FeedService +import com.myapp.testing.TestMicropost +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn +import org.hamcrest.Matchers.* +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.* + +@WebMvcTest(FeedController::class) +class FeedControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var feedService: FeedService + + @Test + fun `findFeed should find feed`() { + val user = signIn() + val now = Date() + `when`(feedService.findFeed(any())).doReturn(listOf(TestMicropost.copy( + content = "content1", + isMyPost = true, + createdAt = now, + user = user.copy( + username = "test1@test.com" + ) + ))) + + perform(get("/api/feed")).apply { + andExpect(status().isOk) + andExpect(jsonPath("$", hasSize(1))) + andExpect(jsonPath("$[0].content", `is`("content1"))) + andExpect(jsonPath("$[0].isMyPost", `is`(true))) + andExpect(jsonPath("$[0].createdAt", greaterThanOrEqualTo(now.time))) + andExpect(jsonPath("$[0].user.email", nullValue())) + andExpect(jsonPath("$[0].user.avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + } + } + + @Test + fun `findFeed should not find feed when not signed in`() { + perform(get("/api/feed")).apply { + andExpect(status().isUnauthorized) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/MicropostControllerTest.kt b/src/test/kotlin/com/myapp/controller/MicropostControllerTest.kt new file mode 100644 index 0000000..1d05a45 --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/MicropostControllerTest.kt @@ -0,0 +1,86 @@ +package com.myapp.controller + +import com.myapp.service.MicropostService +import com.myapp.service.exception.NotAuthorizedException +import com.myapp.testing.TestMicropost +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.doThrow +import com.nhaarman.mockito_kotlin.verify +import org.hamcrest.CoreMatchers.`is` +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(MicropostController::class) +class MicropostControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var micropostService: MicropostService + + @Test + fun `create should create a post`() { + signIn() + `when`(micropostService.create("my content")) + .doReturn(TestMicropost.copy(_id = 1, content = "my content")) + + val response = perform(post("/api/microposts") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj("content" to "my content") + }) + ) + + verify(micropostService).create("my content") + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$.id", `is`(1))) + andExpect(jsonPath("$.content", `is`("my content"))) + } + } + + @Test + fun `create should not create a post when not signed in`() { + perform(post("/api/microposts") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj("content" to "my content") + }) + ).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `delete should delete a post`() { + signIn() + + val response = perform(delete("/api/microposts/1")) + + verify(micropostService).delete(1) + response.andExpect(status().isOk) + } + + @Test + fun `delete should not delete a post when not signed in`() { + perform(delete("/api/microposts/1")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `delete should not delete a post which belongs to others`() { + signIn() + `when`(micropostService.delete(1)).doThrow(NotAuthorizedException("")) + + perform(delete("/api/microposts/1")).apply { + andExpect(status().isForbidden) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/RelatedUserControllerTest.kt b/src/test/kotlin/com/myapp/controller/RelatedUserControllerTest.kt new file mode 100644 index 0000000..693cffb --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/RelatedUserControllerTest.kt @@ -0,0 +1,73 @@ +package com.myapp.controller + +import com.myapp.dto.request.PageParams +import com.myapp.service.RelatedUserService +import com.myapp.testing.TestRelatedUser +import com.nhaarman.mockito_kotlin.* +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers.`is` +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(RelatedUserController::class) +class RelatedUserControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var relatedUserService: RelatedUserService + + @Test + fun `followings should list followings`() { + `when`(relatedUserService.findFollowings(eq(1), any())) + .doReturn(listOf(TestRelatedUser.copy( + name = "John Doe", + username = "test1@test.com", + relationshipId = 101 + ))) + + val response = perform(get("/api/users/1/followings?sinceId=1&maxId=2&count=3")) + + argumentCaptor().apply { + verify(relatedUserService).findFollowings(eq(1), capture()) + assertThat(firstValue.sinceId).isEqualTo(1) + assertThat(firstValue.maxId).isEqualTo(2) + assertThat(firstValue.count).isEqualTo(3) + } + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$[0].name", `is`("John Doe"))) + andExpect(jsonPath("$[0].avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + andExpect(jsonPath("$[0].relationshipId", `is`(101))) + } + } + + @Test + fun `followers should list followers`() { + `when`(relatedUserService.findFollowers(eq(1), any())) + .doReturn(listOf(TestRelatedUser.copy( + name = "John Doe", + username = "test1@test.com", + relationshipId = 101 + ))) + + val response = perform(get("/api/users/1/followers?sinceId=1&maxId=2&count=3")) + + argumentCaptor().apply { + verify(relatedUserService).findFollowers(eq(1), capture()) + assertThat(firstValue.sinceId).isEqualTo(1) + assertThat(firstValue.maxId).isEqualTo(2) + assertThat(firstValue.count).isEqualTo(3) + } + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$[0].name", `is`("John Doe"))) + andExpect(jsonPath("$[0].avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + andExpect(jsonPath("$[0].relationshipId", `is`(101))) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/RelationshipControllerTest.kt b/src/test/kotlin/com/myapp/controller/RelationshipControllerTest.kt new file mode 100644 index 0000000..50fc07a --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/RelationshipControllerTest.kt @@ -0,0 +1,78 @@ +package com.myapp.controller + +import com.myapp.repository.exception.RelationshipDuplicatedException +import com.myapp.service.RelationshipService +import com.myapp.service.exception.RelationshipNotFoundException +import com.nhaarman.mockito_kotlin.doThrow +import com.nhaarman.mockito_kotlin.verify +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(RelationshipController::class) +class RelationshipControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var relationshipService: RelationshipService + + @Test + fun `follow should follow a user`() { + signIn() + + val response = perform(post("/api/relationships/to/1")) + + verify(relationshipService).follow(1) + response.andExpect(status().isOk) + } + + @Test + fun `follow should not follow a user when not signed in`() { + perform(post("/api/relationships/to/1")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `follow should not follow when already followed`() { + signIn() + `when`(relationshipService.follow(1)) + .doThrow(RelationshipDuplicatedException("")) + + perform(post("/api/relationships/to/1")).apply { + andExpect(status().isBadRequest) + } + } + + @Test + fun `unfollow should unfollow a user`() { + signIn() + + val response = perform(delete("/api/relationships/to/1")) + + verify(relationshipService).unfollow(1) + response.andExpect(status().isOk) + } + + @Test + fun `unfollow should not unfollow a user when not signed in`() { + perform(delete("/api/relationships/to/1")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `unfollow should not unfollow when relationship not found`() { + signIn() + `when`(relationshipService.unfollow(1)) + .doThrow(RelationshipNotFoundException()) + + perform(delete("/api/relationships/to/1")).apply { + andExpect(status().isNotFound) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/UserControllerTest.kt b/src/test/kotlin/com/myapp/controller/UserControllerTest.kt new file mode 100644 index 0000000..db9edaf --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/UserControllerTest.kt @@ -0,0 +1,254 @@ +package com.myapp.controller + +import com.myapp.domain.UserStats +import com.myapp.dto.page.PageImpl +import com.myapp.dto.request.UserEditParams +import com.myapp.dto.request.UserNewParams +import com.myapp.repository.exception.EmailDuplicatedException +import com.myapp.repository.exception.RecordNotFoundException +import com.myapp.service.UserService +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.* +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.nullValue +import org.hamcrest.Matchers.hasSize +import org.junit.Test +import org.mockito.Mockito.`when` +import org.slf4j.LoggerFactory +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(UserController::class) +class UserControllerTest : BaseControllerTest() { + + @Suppress("unused") + private val logger = LoggerFactory.getLogger(UserControllerTest::class.java) + + @MockBean + lateinit private var userService: UserService + + @Test + fun `list should list users`() { + signIn() + `when`(userService.findAll(101, 1001)) + .doReturn(PageImpl( + totalPages = 1, + content = listOf(TestUser.copy( + username = "test1@test.com", + name = "John Doe" + )) + )) + + perform(get("/api/users?page=101&size=1001")).apply { + andExpect(status().isOk) + andExpect(jsonPath("$.content").exists()) + andExpect(jsonPath("$.content", hasSize(1))) + andExpect(jsonPath("$.content[0].name", `is`("John Doe"))) + andExpect(jsonPath("$.content[0].email", nullValue())) + andExpect(jsonPath("$.content[0].avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + } + } + + @Test + fun `list should not list when not signed in`() { + perform(get("/api/users")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `create should create a user`() { + `when`(userService.create(any())) + .doReturn(TestUser.copy( + _id = 1, + username = "test1@test.com", + name = "John Doe" + )) + + val response = perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "secret123", + "name" to "John Doe" + ) + }) + ) + + argumentCaptor().apply { + verify(userService).create(capture()) + assertThat(firstValue.email).isEqualTo("test1@test.com") + assertThat(firstValue.password).isEqualTo("secret123") + assertThat(firstValue.name).isEqualTo("John Doe") + } + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$.id", `is`(1))) + andExpect(jsonPath("$.email", nullValue())) + andExpect(jsonPath("$.name", `is`("John Doe"))) + andExpect(jsonPath("$.avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + } + } + + @Test + fun `create should not create a user when password is too short`() { + perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "a", + "name" to "John Doe" + ) + }) + ).apply { + andExpect(status().isBadRequest) + } + } + + @Test + fun `create should not create a user when email is duplicated`() { + `when`(userService.create(any())) + .doThrow(EmailDuplicatedException("")) + + perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "secret123", + "name" to "John Doe" + ) + }) + ).apply { + andExpect(status().isBadRequest) + andExpect(jsonPath("$.code", `is`("email_already_taken"))) + } + } + + @Test + fun `show should show a user`() { + `when`(userService.findOne(1)).doReturn(TestUser.copy( + name = "John Doe", + username = "test1@test.com", + userStats = UserStats( + followerCnt = 1, + followingCnt = 2, + micropostCnt = 3 + ) + )) + + val response = perform(get("/api/users/1")) + + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$.name", `is`("John Doe"))) + andExpect(jsonPath("$.email", nullValue())) + andExpect(jsonPath("$.avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + andExpect(jsonPath("$.isFollowedByMe", nullValue())) + andExpect(jsonPath("$.userStats").exists()) + andExpect(jsonPath("$.userStats.micropostCnt", `is`(3))) + andExpect(jsonPath("$.userStats.followingCnt", `is`(2))) + andExpect(jsonPath("$.userStats.followerCnt", `is`(1))) + } + } + + @Test + fun `show should not show a user when user not found`() { + `when`(userService.findOne(1)).doThrow(RecordNotFoundException()) + + perform(get("/api/users/1")).apply { + andExpect(status().isNotFound) + } + } + + @Test + fun `showMe should show me`() { + signIn() + `when`(userService.findMe()).doReturn(TestUser.copy( + name = "John Doe", + username = "test1@test.com", + userStats = UserStats( + followerCnt = 1, + followingCnt = 2, + micropostCnt = 3 + ) + )) + + val response = perform(get("/api/users/me")) + + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$.name", `is`("John Doe"))) + andExpect(jsonPath("$.email", nullValue())) + andExpect(jsonPath("$.avatarHash", `is`("94fba03762323f286d7c3ca9e001c541"))) + andExpect(jsonPath("$.isFollowedByMe", nullValue())) + andExpect(jsonPath("$.userStats").exists()) + andExpect(jsonPath("$.userStats.micropostCnt", `is`(3))) + andExpect(jsonPath("$.userStats.followingCnt", `is`(2))) + andExpect(jsonPath("$.userStats.followerCnt", `is`(1))) + } + } + + @Test + fun `showMe should not show me when not signed in`() { + perform(get("/api/users/me")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `updateMe should update me`() { + signIn() + + perform(patch("/api/users/me") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "secret123", + "name" to "John Doe" + ) + }) + ) + + argumentCaptor().apply { + verify(userService).updateMe(capture()) + assertThat(firstValue.email).isEqualTo("test1@test.com") + assertThat(firstValue.password).isEqualTo("secret123") + assertThat(firstValue.name).isEqualTo("John Doe") + } + } + + @Test + fun `updateMe should not update me when not signed in`() { + perform(patch("/api/users/me")).apply { + andExpect(status().isUnauthorized) + } + } + + @Test + fun `updateMe should not update me when password is too short`() { + signIn() + + perform(patch("/api/users/me") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonString { + obj( + "email" to "test1@test.com", + "password" to "a", + "name" to "John Doe" + ) + }) + ).apply { + andExpect(status().isBadRequest) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/controller/UserMicropostControllerTest.kt b/src/test/kotlin/com/myapp/controller/UserMicropostControllerTest.kt new file mode 100644 index 0000000..29e276b --- /dev/null +++ b/src/test/kotlin/com/myapp/controller/UserMicropostControllerTest.kt @@ -0,0 +1,87 @@ +package com.myapp.controller + +import com.myapp.dto.request.PageParams +import com.myapp.service.MicropostService +import com.myapp.testing.TestMicropost +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.* +import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.nullValue +import org.hamcrest.Matchers.hasSize +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(UserMicropostController::class) +class UserMicropostControllerTest : BaseControllerTest() { + + @MockBean + lateinit private var micropostService: MicropostService + + @Test + fun `list should list user's posts`() { + `when`(micropostService.findAllByUser(eq(1), any())) + .doReturn(listOf(TestMicropost.copy( + content = "my content", + user = TestUser.copy( + name = "John Doe" + ) + ))) + + val response = perform(get("/api/users/1/microposts/?sinceId=1&maxId=2&count=3")) + + argumentCaptor().apply { + verify(micropostService).findAllByUser(eq(1), capture()) + assertThat(firstValue.sinceId).isEqualTo(1) + assertThat(firstValue.maxId).isEqualTo(2) + assertThat(firstValue.count).isEqualTo(3) + } + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$", hasSize(1))) + andExpect(jsonPath("$[0].content", `is`("my content"))) + andExpect(jsonPath("$[0].isMyPost", nullValue())) + andExpect(jsonPath("$[0].user.name", `is`("John Doe"))) + } + } + + @Test + fun `listMyPosts should list my posts`() { + `when`(micropostService.findMyPosts(any())) + .doReturn(listOf(TestMicropost.copy( + content = "my content", + user = TestUser.copy( + name = "John Doe" + ) + ))) + + val response = perform(get("/api/users/me/microposts/?sinceId=1&maxId=2&count=3")) + + argumentCaptor().apply { + verify(micropostService).findMyPosts(capture()) + assertThat(firstValue.sinceId).isEqualTo(1) + assertThat(firstValue.maxId).isEqualTo(2) + assertThat(firstValue.count).isEqualTo(3) + } + with(response) { + andExpect(status().isOk) + andExpect(jsonPath("$", hasSize(1))) + andExpect(jsonPath("$[0].content", `is`("my content"))) + andExpect(jsonPath("$[0].isMyPost", nullValue())) + andExpect(jsonPath("$[0].user.name", `is`("John Doe"))) + } + } + + @Test + fun `listMyPosts should not list my posts when not signed in`() { + perform(get("/api/users/me/microposts")).apply { + andExpect(status().isUnauthorized) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/repository/BaseRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/BaseRepositoryTest.kt new file mode 100644 index 0000000..eb321ff --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/BaseRepositoryTest.kt @@ -0,0 +1,45 @@ +package com.myapp.repository + +import org.junit.runner.RunWith +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration +import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.BootstrapWith +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.annotation.Transactional +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean +import javax.sql.DataSource +import javax.validation.Validator + +@ActiveProfiles("test") +@RunWith(SpringRunner::class) +@BootstrapWith(SpringBootTestContextBootstrapper::class) +@Transactional +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ComponentScan(basePackages = arrayOf("com.myapp.repository")) +@ImportAutoConfiguration(FlywayAutoConfiguration::class, JooqAutoConfiguration::class) +abstract class BaseRepositoryTest { + + @Configuration + class RepositoryTestConfig { + @Bean + fun validator(): Validator { + return LocalValidatorFactoryBean() + } + + @Bean + @Suppress("SpringKotlinAutowiring") + fun transactionManager(dataSource: DataSource): PlatformTransactionManager { + return DataSourceTransactionManager(dataSource) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/repository/FeedRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/FeedRepositoryTest.kt new file mode 100644 index 0000000..3c8ceaf --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/FeedRepositoryTest.kt @@ -0,0 +1,71 @@ +package com.myapp.repository + +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Micropost.MICROPOST +import com.myapp.generated.tables.Relationship.RELATIONSHIP +import com.myapp.generated.tables.User.USER +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired + +class FeedRepositoryTest : BaseRepositoryTest() { + + @Autowired + lateinit var dsl: DSLContext + + @Autowired + lateinit var feedRepository: FeedRepository + + @Test + fun `findFeed should find a feed`() { + populateTestData() + + val feed = feedRepository.findFeed(1, PageParams()) + + assertThat(feed.size).isEqualTo(3) + assertThat(feed.first().content).isEqualTo("user 2 - content1") + assertThat(feed.last().content).isEqualTo("user 1 - content1") + } + + @Test + fun `findFeed should find a feed with page params`() { + populateTestData() + + feedRepository.findFeed(1, PageParams(sinceId = 2)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().content).isEqualTo("user 2 - content1") + } + feedRepository.findFeed(1, PageParams(maxId = 2)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().content).isEqualTo("user 1 - content1") + } + feedRepository.findFeed(1, PageParams(count = 2)).apply { + assertThat(size).isEqualTo(2) + } + } + + private fun populateTestData() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") + .values(2, "test2@test.com", "secret123", "user 2") + .values(3, "test3@test.com", "secret123", "user 3") + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, FOLLOWER_ID, FOLLOWED_ID) + .values(1, 2) + .execute() + } + with(MICROPOST) { + dsl.insertInto(this, ID, CONTENT, USER_ID) + .values(1, "user 1 - content1", 1) + .values(2, "user 1 - content2", 1) + .values(3, "user 2 - content1", 2) + .values(4, "user 3 - content1", 3) // It won't be included in a feed of user 1. + .execute() + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/repository/MicropostRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/MicropostRepositoryTest.kt new file mode 100644 index 0000000..0791eb4 --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/MicropostRepositoryTest.kt @@ -0,0 +1,125 @@ +package com.myapp.repository + +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Micropost.MICROPOST +import com.myapp.generated.tables.User.USER +import com.myapp.repository.exception.RecordNotFoundException +import com.myapp.testing.TestUser +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import kotlin.test.assertFailsWith + + +class MicropostRepositoryTest : BaseRepositoryTest() { + + @Autowired + lateinit var dsl: DSLContext + + @Autowired + lateinit var micropostRepository: MicropostRepository + + @Test + fun `findOne should find a post`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + dsl.insertInto(MICROPOST, MICROPOST.ID, MICROPOST.CONTENT, MICROPOST.USER_ID) + .values(101, "my content", 1) + .execute() + + val post = micropostRepository.findOne(101) + + assertThat(post).isNotNull() + assertThat(post.content).isEqualTo("my content") + assertThat(post.user.id).isEqualTo(1) + } + + @Test + fun `findOne should throw when no record`() { + assertFailsWith { + micropostRepository.findOne(1) + } + } + + @Test + fun `findAllByUser should find posts by user`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "user1") + .values(2, "test2@test.com", "secret123", "user2") + .execute() + dsl.insertInto(MICROPOST, MICROPOST.ID, MICROPOST.CONTENT, MICROPOST.USER_ID) + .values(101, "my content 1", 1) + .values(102, "my content 2", 1) + .values(103, "user2 content 1", 2) + .execute() + + val posts = micropostRepository.findAllByUser(1, PageParams()) + + assertThat(posts.size).isEqualTo(2) + assertThat(posts.first().content).isEqualTo("my content 2") + assertThat(posts.last().content).isEqualTo("my content 1") + assertThat(posts.first().user.id).isEqualTo(1) + } + + @Test + fun `findAllByUser should find posts with page params`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "user1") + .execute() + dsl.insertInto(MICROPOST, MICROPOST.ID, MICROPOST.CONTENT, MICROPOST.USER_ID) + .values(101, "my content 1", 1) + .values(102, "my content 2", 1) + .values(103, "my content 3", 1) + .execute() + + micropostRepository.findAllByUser(1, PageParams(sinceId = 102)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().content).isEqualTo("my content 3") + } + micropostRepository.findAllByUser(1, PageParams(maxId = 102)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().content).isEqualTo("my content 1") + } + micropostRepository.findAllByUser(1, PageParams(count = 2)).apply { + assertThat(size).isEqualTo(2) + } + } + + @Test + fun `create should create a post`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + + val createdPost = micropostRepository.create(Micropost( + content = "my content", + user = TestUser.copy(_id = 1) + )) + val postRecord = dsl.fetchOne(MICROPOST) + val count = dsl.fetchCount(MICROPOST) + + assertThat(createdPost).isNotNull() + assertThat(createdPost.id).isEqualTo(postRecord.id) + assertThat(count).isEqualTo(1) + } + + @Test + fun `delete should delete a post`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + dsl.insertInto(MICROPOST, MICROPOST.ID, MICROPOST.CONTENT, MICROPOST.USER_ID) + .values(101, "my content", 1) + .execute() + + micropostRepository.delete(101) + val count = dsl.fetchCount(MICROPOST) + + assertThat(count).isEqualTo(0) + } + +} + diff --git a/src/test/kotlin/com/myapp/repository/RelatedUserRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/RelatedUserRepositoryTest.kt new file mode 100644 index 0000000..28267d7 --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/RelatedUserRepositoryTest.kt @@ -0,0 +1,128 @@ +package com.myapp.repository + +import com.myapp.dto.request.PageParams +import com.myapp.generated.tables.Relationship.RELATIONSHIP +import com.myapp.generated.tables.User.USER +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired + +class RelatedUserRepositoryTest : BaseRepositoryTest() { + + @Autowired + lateinit var dsl: DSLContext + + @Autowired + lateinit var relatedUserRepository: RelatedUserRepository + + @Test + fun `findFollowers should find followers`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // subject + .values(2, "test2@test.com", "secret123", "user 2") // follower + .values(3, "test3@test.com", "secret123", "user 3") // follower + .values(4, "test4@test.com", "secret123", "user 4") // not follower + .execute() + } + with(RELATIONSHIP) { dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) .values(102, 2, 1) + .values(103, 3, 1) + .execute() + } + + val followers = relatedUserRepository.findFollowers(1, PageParams()) + + assertThat(followers.size).isEqualTo(2) + assertThat(followers.first().name).isEqualTo("user 3") + assertThat(followers.first().relationshipId).isEqualTo(103) + assertThat(followers.last().relationshipId).isEqualTo(102) + } + + @Test + fun `findFollowers should find followers with page params`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") + .values(2, "test2@test.com", "secret123", "user 2") + .values(3, "test3@test.com", "secret123", "user 3") + .values(4, "test4@test.com", "secret123", "user 4") + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(102, 2, 1) + .values(103, 3, 1) + .values(104, 4, 1) + .execute() + } + + relatedUserRepository.findFollowers(1, PageParams(sinceId = 103)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().username).isEqualTo("test4@test.com") + } + relatedUserRepository.findFollowers(1, PageParams(maxId = 103)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().username).isEqualTo("test2@test.com") + } + relatedUserRepository.findFollowers(1, PageParams(count = 2)).apply { + assertThat(size).isEqualTo(2) + } + } + + @Test + fun `findFollowings should find followings`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // subject + .values(2, "test2@test.com", "secret123", "user 2") // followed + .values(3, "test3@test.com", "secret123", "user 3") // followed + .values(4, "test4@test.com", "secret123", "user 4") // not followed + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(102, 1, 2) + .values(103, 1, 3) + .execute() + } + + val followings = relatedUserRepository.findFollowings(1, PageParams()) + + assertThat(followings.size).isEqualTo(2) + assertThat(followings.first().name).isEqualTo("user 3") + assertThat(followings.first().relationshipId).isEqualTo(103) + assertThat(followings.last().relationshipId).isEqualTo(102) + } + + @Test + fun `findFollowings should find followings with page params`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") + .values(2, "test2@test.com", "secret123", "user 2") + .values(3, "test3@test.com", "secret123", "user 3") + .values(4, "test4@test.com", "secret123", "user 4") + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(102, 1, 2) + .values(103, 1, 3) + .values(104, 1, 4) + .execute() + } + + relatedUserRepository.findFollowings(1, PageParams(sinceId = 103)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().username).isEqualTo("test4@test.com") + } + relatedUserRepository.findFollowings(1, PageParams(maxId = 103)).apply { + assertThat(size).isEqualTo(1) + assertThat(first().username).isEqualTo("test2@test.com") + } + relatedUserRepository.findFollowings(1, PageParams(count = 2)).apply { + assertThat(size).isEqualTo(2) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/repository/RelationshipRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/RelationshipRepositoryTest.kt new file mode 100644 index 0000000..c618fc9 --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/RelationshipRepositoryTest.kt @@ -0,0 +1,145 @@ +package com.myapp.repository + +import com.myapp.domain.Relationship +import com.myapp.generated.tables.Relationship.RELATIONSHIP +import com.myapp.generated.tables.User.USER +import com.myapp.repository.exception.RelationshipDuplicatedException +import com.myapp.testing.TestUser +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import kotlin.test.assertFailsWith + +class RelationshipRepositoryTest : BaseRepositoryTest() { + + @Autowired + lateinit var dsl: DSLContext + + @Autowired + lateinit var relationshipRepository: RelationshipRepository + + @Test + fun `findOneByFollowerAndFollowed should find a relationship by follower and followed`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // follower + .values(2, "test2@test.com", "secret123", "user 2") // followed + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(101, 1, 2) + .execute() + } + + val relationship = relationshipRepository.findOneByFollowerAndFollowed(1, 2) + + assertThat(relationship).isNotNull() + assertThat(relationship?.id).isEqualTo(101) + assertThat(relationship?.follower?.id).isEqualTo(1) + assertThat(relationship?.followed?.id).isEqualTo(2) + } + + @Test + fun `findOneByFollowerAndFollowed should be null when no matched relationship`() { + val relationship = relationshipRepository.findOneByFollowerAndFollowed(1, 2) + + assertThat(relationship).isNull() + } + + @Test + fun `findAllByFollowerAndFollowedUsers should find relationships by follower and followed users`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // follower + .values(2, "test2@test.com", "secret123", "user 2") // followed + .values(3, "test3@test.com", "secret123", "user 3") // followed + .values(4, "test4@test.com", "secret123", "user 4") // followed + .values(5, "test5@test.com", "secret123", "user 5") // others + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(101, 1, 2) + .values(102, 1, 3) + .execute() + } + + val relationships = relationshipRepository + .findAllByFollowerAndFollowedUsers(1, listOf(2, 3, 5)) + + assertThat(relationships.size).isEqualTo(2) + assertThat(relationships.first().id).isEqualTo(101) + assertThat(relationships.first().follower.id).isEqualTo(1) + assertThat(relationships.first().followed.id).isEqualTo(2) + assertThat(relationships.last().id).isEqualTo(102) + assertThat(relationships.last().follower.id).isEqualTo(1) + assertThat(relationships.last().followed.id).isEqualTo(3) + } + + @Test + fun `create should create a relationship`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // follower + .values(2, "test2@test.com", "secret123", "user 2") // followed + .execute() + } + + val createdRelationship = relationshipRepository.create(Relationship( + follower = TestUser.copy(_id = 1), + followed = TestUser.copy(_id = 2) + )) + val relationshipRecord = dsl.fetchOne(RELATIONSHIP) + val count = dsl.fetchCount(RELATIONSHIP) + + assertThat(createdRelationship).isNotNull() + assertThat(createdRelationship.id).isEqualTo(relationshipRecord.id) + assertThat(count).isEqualTo(1) + } + + + @Test + fun `create should throw when relationship is duplicated`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // follower + .values(2, "test2@test.com", "secret123", "user 2") // followed + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(101, 1, 2) + .execute() + } + + assertFailsWith { + relationshipRepository.create(Relationship( + follower = TestUser.copy(_id = 1), + followed = TestUser.copy(_id = 2) + )) + } + } + + @Test + fun `delete should delete a relationship`() { + with(USER) { + dsl.insertInto(this, ID, USERNAME, PASSWORD, NAME) + .values(1, "test1@test.com", "secret123", "user 1") // follower + .values(2, "test2@test.com", "secret123", "user 2") // followed + .execute() + } + with(RELATIONSHIP) { + dsl.insertInto(this, ID, FOLLOWER_ID, FOLLOWED_ID) + .values(101, 1, 2) + .execute() + } + + relationshipRepository.delete(101) + val count = dsl.fetchCount(RELATIONSHIP) + + assertThat(count).isEqualTo(0) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/repository/UserRepositoryTest.kt b/src/test/kotlin/com/myapp/repository/UserRepositoryTest.kt new file mode 100644 index 0000000..30cff3d --- /dev/null +++ b/src/test/kotlin/com/myapp/repository/UserRepositoryTest.kt @@ -0,0 +1,203 @@ +package com.myapp.repository + +import com.myapp.domain.User +import com.myapp.generated.tables.User.USER +import com.myapp.repository.exception.EmailDuplicatedException +import com.myapp.repository.exception.RecordInvalidException +import com.myapp.repository.exception.RecordNotFoundException +import org.assertj.core.api.Assertions.assertThat +import org.jooq.DSLContext +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import kotlin.test.assertFailsWith + + +class UserRepositoryTest : BaseRepositoryTest() { + + @Autowired + lateinit var dsl: DSLContext + + @Autowired + lateinit var userRepository: UserRepository + + @Test + fun `findOne should find a user`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + + val user = userRepository.findOne(1) + + assertThat(user).isNotNull() + assertThat(user.username).isEqualTo("test1@test.com") + } + + @Test + fun `findOne should throw when no record`() { + assertFailsWith { + userRepository.findOne(1) + } + } + + @Test + fun `findOneWithStats should find a user with stats`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .returning(USER.ID) + .fetchOne() + + val user = userRepository.findOneWithStats(1) + + assertThat(user).isNotNull() + assertThat(user.userStats).isNotNull() + assertThat(user.userStats?.micropostCnt).isEqualTo(0) + } + + @Test + fun `findOneWithStats should throw when no record`() { + assertFailsWith { + userRepository.findOneWithStats(1) + } + } + + @Test + fun `findOneByUsername should find user by name`() { + dsl.insertInto(USER, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values("test1@test.com", "secret123", "test1") + .execute() + + val user = userRepository.findOneByUsername("test1@test.com") + + assertThat(user).isNotNull() + assertThat(user?.username).isEqualTo("test1@test.com") + } + + @Test + fun `findOneByUsername should be null when no matched user`() { + val user = userRepository.findOneByUsername("test1@test.com") + + assertThat(user).isNull() + } + + @Test + fun `findAll should find users`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .values(2, "test2@test.com", "secret123", "test2") + .execute() + + val page = userRepository.findAll(page = 1, size = 10) + + assertThat(page.content.size).isEqualTo(2) + assertThat(page.content.first().username).isEqualTo("test1@test.com") + assertThat(page.content.last().username).isEqualTo("test2@test.com") + assertThat(page.totalPages).isEqualTo(1) + } + + @Test + fun `findAll should find users on page 2`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .values(2, "test2@test.com", "secret123", "test2") + .execute() + + val page = userRepository.findAll(page = 2, size = 1) + + assertThat(page.content.size).isEqualTo(1) + assertThat(page.content.first().username).isEqualTo("test2@test.com") + assertThat(page.totalPages).isEqualTo(2) + } + + @Test + fun `create should create a user`() { + val createdUser = userRepository.create(User( + username = "test1@test.com", + password = "secret123", + name = "test1" + )) + val userRecord = dsl.fetchOne(USER) + val count = dsl.fetchCount(USER) + + assertThat(createdUser).isNotNull() + assertThat(createdUser.id).isEqualTo(userRecord.id) + assertThat(count).isEqualTo(1) + } + + @Test + fun `create should not create a user when it is invalid`() { + assertFailsWith { + userRepository.create(User( + username = "test1 AT test.com", + password = "secret123", + name = "test1" + )) + } + } + + @Test + fun `create should throw when email is duplicated`() { + dsl.insertInto(USER, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values("test1@test.com", "secret123", "test1") + .execute() + + assertFailsWith { + userRepository.create(User( + username = "test1@test.com", + password = "secret123", + name = "test1" + )) + } + } + + @Test + fun `update should update a user`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + + userRepository.update(User( + _id = 1, + username = "test1@test.com", + password = "secret123", + name = "test2" + )) + val userRecord = dsl.fetchOne(USER) + + assertThat(userRecord.name).isEqualTo("test2") + } + + @Test + fun `update should not update a user when it is invalid`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .execute() + + assertFailsWith { + userRepository.update(User( + _id = 1, + username = "test1 AT test.com", + password = "secret123", + name = "test2" + )) + } + } + + @Test + fun `update should throw when email is duplicated`() { + dsl.insertInto(USER, USER.ID, USER.USERNAME, USER.PASSWORD, USER.NAME) + .values(1, "test1@test.com", "secret123", "test1") + .values(2, "test2@test.com", "secret123", "test2") + .execute() + + assertFailsWith { + userRepository.update(User( + _id = 2, + username = "test1@test.com", + password = "secret123", + name = "test2" + )) + } + } + +} + diff --git a/src/test/kotlin/com/myapp/service/FeedServiceTest.kt b/src/test/kotlin/com/myapp/service/FeedServiceTest.kt new file mode 100644 index 0000000..0c5dd9e --- /dev/null +++ b/src/test/kotlin/com/myapp/service/FeedServiceTest.kt @@ -0,0 +1,55 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.dto.request.PageParams +import com.myapp.repository.FeedRepository +import com.myapp.testing.TestMicropost +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.security.access.AccessDeniedException +import kotlin.test.assertFailsWith + +class FeedServiceTest { + + private val feedRepository: FeedRepository = mock() + private val securityContextService: SecurityContextService = mock() + private val feedService by lazy { + FeedServiceImpl( + feedRepository = feedRepository, + securityContextService = securityContextService + ) + } + + @Test + fun `findFeed should find feed`() { + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(feedRepository.findFeed(1, PageParams())) + .doReturn(listOf( + TestMicropost.copy( + user = TestUser.copy(_id = 1) + ), + TestMicropost.copy( + user = TestUser.copy(_id = 2) + ) + )) + + val feed = feedService.findFeed(PageParams()) + + assertThat(feed.size).isEqualTo(2) + assertThat(feed.first().isMyPost).isEqualTo(true) + assertThat(feed.last().isMyPost).isEqualTo(false) + } + + @Test + fun `findFeed should throw when not signed in`() { + assertFailsWith { + feedService.findFeed(PageParams()) + } + } + +} diff --git a/src/test/kotlin/com/myapp/service/MicropostServiceTest.kt b/src/test/kotlin/com/myapp/service/MicropostServiceTest.kt new file mode 100644 index 0000000..2610136 --- /dev/null +++ b/src/test/kotlin/com/myapp/service/MicropostServiceTest.kt @@ -0,0 +1,144 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Micropost +import com.myapp.dto.request.PageParams +import com.myapp.repository.MicropostRepository +import com.myapp.service.exception.NotAuthorizedException +import com.myapp.testing.TestMicropost +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.security.access.AccessDeniedException +import kotlin.test.assertFailsWith + +class MicropostServiceTest { + + private val micropostRepository: MicropostRepository = mock() + private val securityContextService: SecurityContextService = mock() + private val micropostService by lazy { + MicropostServiceImpl( + micropostRepository = micropostRepository, + securityContextService = securityContextService + ) + } + + @Test + fun `findAllByUser should find posts by user when signed in and the posts belongs to current user`() { + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(micropostRepository.findAllByUser(1, PageParams())) + .doReturn(listOf(TestMicropost)) + + micropostService.findAllByUser(1, PageParams()).apply { + assertThat(size).isEqualTo(1) + assertThat(first().isMyPost).isEqualTo(true) + } + } + + @Test + fun `findAllByUser should find posts by user when signed in and the posts belongs to another user`() { + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(micropostRepository.findAllByUser(2, PageParams())) + .doReturn(listOf(TestMicropost)) + + micropostService.findAllByUser(2, PageParams()).apply { + assertThat(size).isEqualTo(1) + assertThat(first().isMyPost).isEqualTo(false) + } + } + + @Test + fun `findAllByUser should find posts by user when not signed`() { + `when`(micropostRepository.findAllByUser(1, PageParams())) + .doReturn(listOf(TestMicropost)) + + micropostService.findAllByUser(1, PageParams()).apply { + assertThat(size).isEqualTo(1) + assertThat(first().isMyPost).isNull() + } + } + + @Test + fun `findMyPosts should find my posts`() { + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(micropostRepository.findAllByUser(1, PageParams())) + .doReturn(listOf(TestMicropost)) + + micropostService.findMyPosts(PageParams()).apply { + assertThat(size).isEqualTo(1) + assertThat(first().isMyPost).isEqualTo(true) + } + } + + @Test + fun `findMyPosts should throw when not signed in`() { + assertFailsWith { + micropostService.findMyPosts(PageParams()) + } + } + + @Test + fun `create should create a post`() { + `when`(securityContextService.currentUser()).doReturn(TestUser) + + micropostService.create("my test content") + + argumentCaptor().apply { + verify(micropostRepository).create(capture()) + assertThat(firstValue.content).isEqualTo("my test content") + assertThat(firstValue.user).isEqualTo(TestUser) + } + } + + @Test + fun `create should throw when not signed in`() { + assertFailsWith { + micropostService.create("my test content") + } + } + + @Test + fun `delete should delete a post`() { + val currentUser = TestUser.copy(_id = 1) + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(micropostRepository.findOne(101)) + .doReturn(TestMicropost.copy( + user = currentUser + )) + + micropostService.delete(101) + + verify(micropostRepository).delete(101) + } + + @Test + fun `delete should not delete a post when the post does not belong to current user`() { + `when`(securityContextService.currentUser()) + .doReturn(TestUser.copy(_id = 1)) + `when`(micropostRepository.findOne(101)) + .doReturn(TestMicropost.copy( + user = TestUser.copy(_id = 2) + )) + + assertFailsWith { + micropostService.delete(101) + } + } + + @Test + fun `delete should throw when not signed in`() { + assertFailsWith { + micropostService.delete(1) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/service/RelatedUserServiceTest.kt b/src/test/kotlin/com/myapp/service/RelatedUserServiceTest.kt new file mode 100644 index 0000000..eb8ff66 --- /dev/null +++ b/src/test/kotlin/com/myapp/service/RelatedUserServiceTest.kt @@ -0,0 +1,105 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Relationship +import com.myapp.dto.request.PageParams +import com.myapp.repository.RelatedUserRepository +import com.myapp.repository.RelationshipRepository +import com.myapp.testing.TestRelatedUser +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` + +class RelatedUserServiceTest { + + private val relatedUserRepository: RelatedUserRepository = mock() + private val relationshipRepository: RelationshipRepository = mock() + private val securityContextService: SecurityContextService = mock() + private val relatedUserService by lazy { + RelatedUserServiceImpl( + relatedUserRepository = relatedUserRepository, + relationshipRepository = relationshipRepository, + securityContextService = securityContextService + ) + } + + @Test + fun `findFollowers should find followers`() { + val user = TestUser.copy(_id = 1) + val currentUser = TestUser.copy(_id = 2) + val followers = listOf( + TestRelatedUser.copy(_id = 101), + TestRelatedUser.copy(_id = 102) + ) + val currentUserRelationships = listOf( + Relationship(follower = currentUser, followed = TestUser.copy(_id = 101)) + ) + `when`(securityContextService.currentUser()) + .doReturn(currentUser) + `when`(relatedUserRepository.findFollowers(user.id, PageParams())) + .doReturn(followers) + `when`(relationshipRepository.findAllByFollowerAndFollowedUsers(currentUser.id, followers.map { it.id })) + .doReturn(currentUserRelationships) + + val foundFollowers = relatedUserService.findFollowers(user.id, PageParams()) + + assertThat(foundFollowers.size).isEqualTo(2) + assertThat(foundFollowers.first().isFollowedByMe).isEqualTo(true) + assertThat(foundFollowers.last().isFollowedByMe).isEqualTo(false) + } + + @Test + fun `findFollowers should find followers when not signed in`() { + val user = TestUser.copy(_id = 1) + val followers = listOf(TestRelatedUser.copy(_id = 101)) + `when`(relatedUserRepository.findFollowers(user.id, PageParams())) + .doReturn(followers) + + val foundFollowers = relatedUserService.findFollowers(user.id, PageParams()) + + assertThat(foundFollowers.size).isEqualTo(1) + assertThat(foundFollowers.first().isFollowedByMe).isNull() + } + + @Test + fun `findFollowings should find followings`() { + val user = TestUser.copy(_id = 1) + val currentUser = TestUser.copy(_id = 2) + val followings = listOf( + TestRelatedUser.copy(_id = 101), + TestRelatedUser.copy(_id = 102) + ) + val currentUserRelationships = listOf( + Relationship(follower = currentUser, followed = TestUser.copy(_id = 101)) + ) + `when`(securityContextService.currentUser()) + .doReturn(currentUser) + `when`(relatedUserRepository.findFollowings(user.id, PageParams())) + .doReturn(followings) + `when`(relationshipRepository.findAllByFollowerAndFollowedUsers(currentUser.id, followings.map { it.id })) + .doReturn(currentUserRelationships) + + val foundFollowings = relatedUserService.findFollowings(user.id, PageParams()) + + assertThat(foundFollowings.size).isEqualTo(2) + assertThat(foundFollowings.first().isFollowedByMe).isEqualTo(true) + assertThat(foundFollowings.last().isFollowedByMe).isEqualTo(false) + } + + @Test + fun `findFollowings should find followings when not signed in`() { + val user = TestUser.copy(_id = 1) + val followings = listOf(TestRelatedUser.copy(_id = 101)) + `when`(relatedUserRepository.findFollowings(user.id, PageParams())) + .doReturn(followings) + + val foundFollowings = relatedUserService.findFollowings(user.id, PageParams()) + + assertThat(foundFollowings.size).isEqualTo(1) + assertThat(foundFollowings.first().isFollowedByMe).isNull() + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/service/RelationshipServiceTest.kt b/src/test/kotlin/com/myapp/service/RelationshipServiceTest.kt new file mode 100644 index 0000000..6e11352 --- /dev/null +++ b/src/test/kotlin/com/myapp/service/RelationshipServiceTest.kt @@ -0,0 +1,70 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Relationship +import com.myapp.repository.RelationshipRepository +import com.myapp.repository.UserRepository +import com.myapp.service.exception.RelationshipNotFoundException +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import kotlin.test.assertFailsWith + +class RelationshipServiceTest { + + private val userRepository: UserRepository = mock() + private val relationshipRepository: RelationshipRepository = mock() + private val securityContextService: SecurityContextService = mock() + private val relationshipService by lazy { + RelationshipServiceImpl( + userRepository = userRepository, + relationshipRepository = relationshipRepository, + securityContextService = securityContextService + ) + } + + @Test + fun `follow should follow user`() { + val user = TestUser.copy() + val currentUser = TestUser.copy() + `when`(userRepository.findOne(1)).doReturn(user) + `when`(securityContextService.currentUser()).doReturn(currentUser) + + relationshipService.follow(1) + + argumentCaptor().apply { + verify(relationshipRepository).create(capture()) + assertThat(firstValue.follower).isEqualTo(currentUser) + assertThat(firstValue.followed).isEqualTo(user) + } + } + + @Test + fun `unfollow should unfollow user`() { + val currentUser = TestUser.copy() + `when`(securityContextService.currentUser()).doReturn(currentUser) + `when`(relationshipRepository.findOneByFollowerAndFollowed(currentUser.id, 1)) + .doReturn(Relationship( + _id = 101, + follower = currentUser, + followed = TestUser.copy() + )) + + relationshipService.unfollow(1) + + verify(relationshipRepository).delete(101) + } + + @Test + fun `unfollow should not unfollow user when no relationship`() { + val currentUser = TestUser.copy() + `when`(securityContextService.currentUser()).doReturn(currentUser) + + assertFailsWith { + relationshipService.unfollow(1) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/service/UserDetailsServiceTest.kt b/src/test/kotlin/com/myapp/service/UserDetailsServiceTest.kt new file mode 100644 index 0000000..384119b --- /dev/null +++ b/src/test/kotlin/com/myapp/service/UserDetailsServiceTest.kt @@ -0,0 +1,40 @@ +package com.myapp.service + +import com.myapp.repository.UserRepository +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.security.core.userdetails.UsernameNotFoundException +import kotlin.test.assertFailsWith + +class UserDetailsServiceTest { + + private val userRepository: UserRepository = mock() + private val userDetailsService by lazy { + UserDetailsServiceImpl( + userRepository = userRepository + ) + } + + @Test + fun `loadUserByUsername should load user by username`() { + `when`(userRepository.findOneByUsername("test1@test.com")) + .doReturn(TestUser) + + val userDetails = userDetailsService.loadUserByUsername("test1@test.com") + + assertThat(userDetails).isNotNull() + assertThat(userDetails.user).isEqualTo(TestUser) + } + + @Test + fun `loadUserByUsername should throw when username was not found`() { + assertFailsWith { + userDetailsService.loadUserByUsername("test1@test.com") + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/service/UserServiceTest.kt b/src/test/kotlin/com/myapp/service/UserServiceTest.kt new file mode 100644 index 0000000..e66bee4 --- /dev/null +++ b/src/test/kotlin/com/myapp/service/UserServiceTest.kt @@ -0,0 +1,176 @@ +package com.myapp.service + +import com.myapp.auth.SecurityContextService +import com.myapp.domain.Relationship +import com.myapp.domain.User +import com.myapp.dto.page.PageImpl +import com.myapp.dto.request.UserEditParams +import com.myapp.dto.request.UserNewParams +import com.myapp.repository.RelationshipRepository +import com.myapp.repository.UserRepository +import com.myapp.testing.TestUser +import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.Mockito.`when` +import org.springframework.security.access.AccessDeniedException +import kotlin.test.assertFailsWith + + +class UserServiceTest { + + private val userRepository: UserRepository = mock() + private val relationshipRepository: RelationshipRepository = mock() + private val securityContextService: SecurityContextService = mock() + private val userService by lazy { + UserServiceImpl( + userRepository = userRepository, + relationshipRepository = relationshipRepository, + securityContextService = securityContextService + ) + } + + @Test + fun `findOne should find a user who is followed by current user`() { + val currentUser = TestUser.copy(_id = 1) + val user = TestUser.copy(_id = 2) + `when`(securityContextService.currentUser()) + .doReturn(currentUser) + `when`(userRepository.findOneWithStats(2)) + .doReturn(user) + `when`(relationshipRepository.findOneByFollowerAndFollowed(1, 2)) + .doReturn(Relationship( + follower = currentUser, + followed = user + )) + + userService.findOne(2).apply { + assertThat(id).isEqualTo(2) + assertThat(isFollowedByMe).isEqualTo(true) + assertThat(isMyself).isEqualTo(false) + } + } + + @Test + fun `findOne should find a user who is current user itself`() { + val currentUser = TestUser.copy(_id = 1) + val user = currentUser + `when`(securityContextService.currentUser()) + .doReturn(currentUser) + `when`(userRepository.findOneWithStats(1)) + .doReturn(user) + + userService.findOne(1).apply { + assertThat(id).isEqualTo(1) + assertThat(isMyself).isEqualTo(true) + assertThat(isFollowedByMe).isEqualTo(false) + } + } + + @Test + fun `findOne should find a user when not signed in`() { + `when`(userRepository.findOneWithStats(1)) + .doReturn(TestUser.copy(_id = 1)) + + userService.findOne(1).apply { + assertThat(id).isEqualTo(1) + assertThat(isMyself).isNull() + assertThat(isFollowedByMe).isNull() + } + } + + @Test + fun `findMe should find me`() { + val currentUser = TestUser.copy(_id = 1) + val user = currentUser + `when`(securityContextService.currentUser()) + .doReturn(currentUser) + `when`(userRepository.findOneWithStats(1)) + .doReturn(user) + + userService.findMe().apply { + assertThat(id).isEqualTo(1) + assertThat(isMyself).isEqualTo(true) + assertThat(isFollowedByMe).isEqualTo(false) + } + } + + @Test + fun `findMe should throw when not signed in`() { + assertFailsWith { + userService.findMe() + } + } + + @Test + fun `findAll should find users`() { + val page = PageImpl( + content = listOf(TestUser), + totalPages = 1 + ) + `when`(userRepository.findAll(1, 1)).doReturn(page) + + userService.findAll(1, 1).apply { + assertThat(this).isEqualTo(page) + } + } + + @Test + fun `create should create a user`() { + userService.create(UserNewParams( + email = "test1@test.com", + password = "secret123", + name = "John Doe" + )) + + argumentCaptor().apply { + verify(userRepository).create(capture()) + assertThat(firstValue.username).isEqualTo("test1@test.com") + assertThat(firstValue.password).matches("^\\$2[ayb]\\$.{56}$") + assertThat(firstValue.name).isEqualTo("John Doe") + } + } + + @Test + fun `updateMe should update me`() { + `when`(securityContextService.currentUser()).doReturn(TestUser) + + userService.updateMe(UserEditParams( + email = "test1@test.com", + password = "secret123", + name = "John Doe" + )) + + argumentCaptor().apply { + verify(userRepository).update(capture()) + assertThat(firstValue.username).isEqualTo("test1@test.com") + assertThat(firstValue.password).matches("^\\$2[ayb]\\$.{56}$") + assertThat(firstValue.name).isEqualTo("John Doe") + } + } + + @Test + fun `updateMe should update me with blank parameters`() { + `when`(securityContextService.currentUser()).doReturn(TestUser) + + userService.updateMe(UserEditParams()) + + argumentCaptor().apply { + verify(userRepository).update(capture()) + assertThat(firstValue.username).isEqualTo(TestUser.username) + assertThat(firstValue.password).isEqualTo(TestUser.password) + assertThat(firstValue.name).isEqualTo(TestUser.name) + } + } + + @Test + fun `updateMe should throw when not signed in`() { + assertFailsWith { + userService.updateMe(UserEditParams()) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/myapp/testing/TestUtil.kt b/src/test/kotlin/com/myapp/testing/TestUtil.kt new file mode 100644 index 0000000..5a7f332 --- /dev/null +++ b/src/test/kotlin/com/myapp/testing/TestUtil.kt @@ -0,0 +1,24 @@ +package com.myapp.testing + +import com.myapp.domain.Micropost +import com.myapp.domain.RelatedUser +import com.myapp.domain.User + +val TestUser = User( + _id = 1, + username = "test@test.com", + password = "encrypted password", + name = "John Doe" +) +val TestMicropost = Micropost( + _id = 1, + content = "test content", + user = TestUser +) +val TestRelatedUser = RelatedUser( + _id = 1, + username = "test@test.com", + name = "John Doe", + relationshipId = 1 +) + From a8262a52f34d9841e90d22a98670757ca09dc2cc Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Wed, 1 Mar 2017 18:30:14 +0700 Subject: [PATCH 02/17] Add QA on readme about run or debug --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index da22d75..7394fb8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ After you migrated DB. * Q) IntelliJ IDEA is very slow when you use jOOQ with Kotlin. * A) Refer [this ticket](https://youtrack.jetbrains.com/issue/KT-10978). In my case, [tuning memory config](https://youtrack.jetbrains.com/issue/KT-10978#comment=27-1519260) of IntelliJ IDEA worked. +* Q) How can I run or debug app from IntelliJ IDEA? +* A) Use IntelliJ IDEA 2017.1 and run or debug Application.kt. + ## Docker Support Dev From e9880788aba0f46a7f89283e32e2242aa8f61838 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Wed, 1 Mar 2017 20:08:35 +0700 Subject: [PATCH 03/17] travis --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 90d7f34..902b600 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,10 @@ install: - pip install awscli - aws --version -script: ./gradlew clean jooqGenerate build jacocoTestReport coveralls +script: + - pwd + - ./gradlew clean jooqGenerate build jacocoTestReport coveralls + - ls -R before_deploy: # Parse branch name and determine an environment to deploy From f25c965df2639453b69d60f8b51533e1060072af Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Wed, 1 Mar 2017 20:15:18 +0700 Subject: [PATCH 04/17] fix gitignore to include migration script --- .gitignore | 2 +- .travis.yml | 2 - .../db/migration/V1__create-schema.sql | 37 +++++++++++++++++++ .../db/migration/V2__create_feed_view.sql | 14 +++++++ .../migration/V3__create_user_stats_view.sql | 14 +++++++ .../V4__set_micropost_default_created_at.sql | 2 + 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/migration/V1__create-schema.sql create mode 100644 src/main/resources/db/migration/V2__create_feed_view.sql create mode 100644 src/main/resources/db/migration/V3__create_user_stats_view.sql create mode 100644 src/main/resources/db/migration/V4__set_micropost_default_created_at.sql diff --git a/.gitignore b/.gitignore index eb82cb9..ddb5851 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /build/ !gradle/wrapper/gradle-wrapper.jar generated/ -db/ +/db/ .envrc ### STS ### diff --git a/.travis.yml b/.travis.yml index 902b600..180c8ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,7 @@ install: - aws --version script: - - pwd - ./gradlew clean jooqGenerate build jacocoTestReport coveralls - - ls -R before_deploy: # Parse branch name and determine an environment to deploy diff --git a/src/main/resources/db/migration/V1__create-schema.sql b/src/main/resources/db/migration/V1__create-schema.sql new file mode 100644 index 0000000..b5a267f --- /dev/null +++ b/src/main/resources/db/migration/V1__create-schema.sql @@ -0,0 +1,37 @@ +CREATE TABLE `user` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(30) NOT NULL, + `password` VARCHAR(100) NOT NULL, + `username` VARCHAR(30) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UK_username` (`username`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8; + +CREATE TABLE `micropost` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `content` VARCHAR(255) NOT NULL, + `created_at` DATETIME NOT NULL, + `user_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_user` (`user_id`), + CONSTRAINT `FK_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8; + +CREATE TABLE `relationship` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `followed_id` BIGINT(20) NOT NULL, + `follower_id` BIGINT(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_followed` (`followed_id`), + KEY `FK_follower` (`follower_id`), + CONSTRAINT `FK_follower` FOREIGN KEY (`follower_id`) REFERENCES `user` (`id`), + CONSTRAINT `FK_followed` FOREIGN KEY (`followed_id`) REFERENCES `user` (`id`), + UNIQUE KEY `UK_follower_followed` (`follower_id`, `followed_id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8; + diff --git a/src/main/resources/db/migration/V2__create_feed_view.sql b/src/main/resources/db/migration/V2__create_feed_view.sql new file mode 100644 index 0000000..232bcc3 --- /dev/null +++ b/src/main/resources/db/migration/V2__create_feed_view.sql @@ -0,0 +1,14 @@ +CREATE VIEW `feed` AS + SELECT + m.*, + r.follower_id AS feed_user_id + FROM micropost m + INNER JOIN relationship r + ON m.user_id = r.followed_id + UNION + SELECT + m.*, + u.id AS feed_user_id + FROM micropost m + INNER JOIN user u + ON m.user_id = u.id; diff --git a/src/main/resources/db/migration/V3__create_user_stats_view.sql b/src/main/resources/db/migration/V3__create_user_stats_view.sql new file mode 100644 index 0000000..ccc1402 --- /dev/null +++ b/src/main/resources/db/migration/V3__create_user_stats_view.sql @@ -0,0 +1,14 @@ +CREATE VIEW `user_stats` AS + SELECT + u.id AS user_id, + (SELECT count(*) + FROM relationship r + WHERE r.followed_id = u.id) AS follower_cnt, + (SELECT count(*) + FROM relationship r + WHERE r.follower_id = u.id) AS following_cnt, + (SELECT count(*) + FROM micropost m + WHERE m.user_id = u.id) AS micropost_cnt + FROM user u + diff --git a/src/main/resources/db/migration/V4__set_micropost_default_created_at.sql b/src/main/resources/db/migration/V4__set_micropost_default_created_at.sql new file mode 100644 index 0000000..af1ee25 --- /dev/null +++ b/src/main/resources/db/migration/V4__set_micropost_default_created_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE micropost + CHANGE created_at created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; From 2ef7352331c51f5866af26129d45be3256431415 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Wed, 1 Mar 2017 20:38:23 +0700 Subject: [PATCH 05/17] format readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7394fb8..98d21b5 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ After you migrated DB. ## Frequently asked questions * Q) IntelliJ IDEA is very slow when you use jOOQ with Kotlin. -* A) Refer [this ticket](https://youtrack.jetbrains.com/issue/KT-10978). In my case, [tuning memory config](https://youtrack.jetbrains.com/issue/KT-10978#comment=27-1519260) of IntelliJ IDEA worked. + * A) Refer [this ticket](https://youtrack.jetbrains.com/issue/KT-10978). In my case, [tuning memory config](https://youtrack.jetbrains.com/issue/KT-10978#comment=27-1519260) of IntelliJ IDEA worked. * Q) How can I run or debug app from IntelliJ IDEA? -* A) Use IntelliJ IDEA 2017.1 and run or debug Application.kt. + * A) Use IntelliJ IDEA 2017.1 and run or debug Application.kt. ## Docker Support From eff6d539d13c0f8c972893aec77e18d0bfdcd966 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 09:35:17 +0700 Subject: [PATCH 06/17] use cobertura instead of jacoco I want to exclude hashCode() and etc which are automatically generated by Kotlin data class. --- README.md | 2 +- build.gradle | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 98d21b5..a7175b8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ After you migrated DB. ## Frequently asked questions -* Q) IntelliJ IDEA is very slow when you use jOOQ with Kotlin. +* Q) IntelliJ IDEA is very slow when I use jOOQ with Kotlin. * A) Refer [this ticket](https://youtrack.jetbrains.com/issue/KT-10978). In my case, [tuning memory config](https://youtrack.jetbrains.com/issue/KT-10978#comment=27-1519260) of IntelliJ IDEA worked. * Q) How can I run or debug app from IntelliJ IDEA? diff --git a/build.gradle b/build.gradle index 1cab3c8..3be710a 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,6 @@ buildscript { repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } - maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' } } dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" @@ -28,7 +27,7 @@ buildscript { } plugins { - id 'jacoco' + id 'net.saliman.cobertura' version '2.4.0' id 'com.github.kt3k.coveralls' version '2.8.1' } apply plugin: 'java' @@ -47,7 +46,6 @@ sourceCompatibility = 1.8 repositories { jcenter() - maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.1' } } dependencies { @@ -75,15 +73,13 @@ dependencies { testCompile group: 'com.beust', name: 'klaxon', version: '0.27' } -jacocoTestReport { - reports { - xml.enabled = true // coveralls plugin depends on xml format report - html.enabled = true - } +cobertura { + coverageFormats = ['html', 'xml'] + coverageExcludes = ['.*com.myapp.generated.*'] } flyway { - url = 'jdbc:h2:./db/dev;MODE=MySQL' + url = 'jdbc:h2:./db/tmp;MODE=MySQL' user = 'sa' password = '' } @@ -95,7 +91,7 @@ task jooqGenerate { .configuration('xmlns': "http://www.jooq.org/xsd/jooq-codegen-${jooqVersion}.xsd") { jdbc { driver 'org.h2.Driver' - url 'jdbc:h2:./db/dev;MODE=MySQL' + url 'jdbc:h2:./db/tmp;MODE=MySQL' user 'sa' password '' } From 17c9ad928115e73fe59173d4a379b028b3c530dc Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 09:43:17 +0700 Subject: [PATCH 07/17] fix travis yml to use cobertura --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 180c8ff..e090e82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - aws --version script: - - ./gradlew clean jooqGenerate build jacocoTestReport coveralls + - ./gradlew clean jooqGenerate build cobertura coveralls before_deploy: # Parse branch name and determine an environment to deploy From 4f0973f7a5d8eb884ee1a0074fea2fba44fb0d1b Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 10:58:55 +0700 Subject: [PATCH 08/17] send coverage --- .travis.yml | 2 +- build.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e090e82..8108263 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: - aws --version script: - - ./gradlew clean jooqGenerate build cobertura coveralls + - ./gradlew clean jooqGenerate build cobertura coveralls | grep -v DEBUG # I don't know why cobertura is DEBUG mode, though it is INFO level in my PC. before_deploy: # Parse branch name and determine an environment to deploy diff --git a/build.gradle b/build.gradle index 3be710a..2d9a799 100644 --- a/build.gradle +++ b/build.gradle @@ -76,6 +76,7 @@ dependencies { cobertura { coverageFormats = ['html', 'xml'] coverageExcludes = ['.*com.myapp.generated.*'] + coverageSourceDirs = sourceSets.main.kotlin.srcDirs } flyway { From 7ef634c6de51296d719fd745b4cdcaa9ce9444a7 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 11:25:07 +0700 Subject: [PATCH 09/17] try openjdk --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8108263..59dca35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ sudo: required dist: trusty language: java jdk: -- oraclejdk8 +#- oraclejdk8 +- openjdk8 services: - docker @@ -13,7 +14,7 @@ install: - aws --version script: - - ./gradlew clean jooqGenerate build cobertura coveralls | grep -v DEBUG # I don't know why cobertura is DEBUG mode, though it is INFO level in my PC. + - ./gradlew clean jooqGenerate build cobertura coveralls before_deploy: # Parse branch name and determine an environment to deploy From fe493617741665a875c3561661b07e5130499e84 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 11:36:59 +0700 Subject: [PATCH 10/17] try jdk7 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 59dca35..15450b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ dist: trusty language: java jdk: #- oraclejdk8 -- openjdk8 +- oraclejdk7 +#- openjdk8 services: - docker From 887f4990f09cbd09e6b4665949f6451993357a0a Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 11:38:49 +0700 Subject: [PATCH 11/17] try some options for Cobertura --- .travis.yml | 4 ++-- build.gradle | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 15450b8..abcb753 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ sudo: required dist: trusty language: java jdk: -#- oraclejdk8 -- oraclejdk7 +- oraclejdk8 +#- oraclejdk7 #- openjdk8 services: - docker diff --git a/build.gradle b/build.gradle index 2d9a799..3453808 100644 --- a/build.gradle +++ b/build.gradle @@ -77,6 +77,8 @@ cobertura { coverageFormats = ['html', 'xml'] coverageExcludes = ['.*com.myapp.generated.*'] coverageSourceDirs = sourceSets.main.kotlin.srcDirs + coverageIgnoreTrivial = true + coverageIgnores = [] } flyway { From 614c6cc057d837f502d4fc2c63ad28eda4c4ae65 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 11:47:31 +0700 Subject: [PATCH 12/17] add testRuntime --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 3453808..e8a6f97 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,7 @@ dependencies { testCompile group: 'org.assertj', name: 'assertj-core', version: '3.6.2' testCompile group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.1.0' testCompile group: 'com.beust', name: 'klaxon', version: '0.27' + testRuntime "org.slf4j:slf4j-api:1.7.10" // for gradle-cobertura-plugin } cobertura { From e747c630fa7cde8b439a05dffc4b1a4bbcf2419b Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 11:55:32 +0700 Subject: [PATCH 13/17] remove coverage spport once... --- .travis.yml | 4 +--- build.gradle | 13 ------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index abcb753..58d73fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ dist: trusty language: java jdk: - oraclejdk8 -#- oraclejdk7 -#- openjdk8 services: - docker @@ -15,7 +13,7 @@ install: - aws --version script: - - ./gradlew clean jooqGenerate build cobertura coveralls + - ./gradlew clean jooqGenerate build before_deploy: # Parse branch name and determine an environment to deploy diff --git a/build.gradle b/build.gradle index e8a6f97..6a85aa2 100644 --- a/build.gradle +++ b/build.gradle @@ -26,10 +26,6 @@ buildscript { } } -plugins { - id 'net.saliman.cobertura' version '2.4.0' - id 'com.github.kt3k.coveralls' version '2.8.1' -} apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'kotlin-spring' @@ -71,15 +67,6 @@ dependencies { testCompile group: 'org.assertj', name: 'assertj-core', version: '3.6.2' testCompile group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.1.0' testCompile group: 'com.beust', name: 'klaxon', version: '0.27' - testRuntime "org.slf4j:slf4j-api:1.7.10" // for gradle-cobertura-plugin -} - -cobertura { - coverageFormats = ['html', 'xml'] - coverageExcludes = ['.*com.myapp.generated.*'] - coverageSourceDirs = sourceSets.main.kotlin.srcDirs - coverageIgnoreTrivial = true - coverageIgnores = [] } flyway { From 4769d5b0f58794528e93ce3b6d8fbaa269dfd7d8 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Thu, 2 Mar 2017 14:15:19 +0700 Subject: [PATCH 14/17] kotlin 1.1.0 release --- build.gradle | 2 +- src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 6a85aa2..4e5c513 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ import javax.xml.bind.JAXB buildscript { ext { - kotlinVersion = '1.0.6' + kotlinVersion = '1.1.0' springBootVersion = '1.5.1.RELEASE' jooqVersion = '3.9.0' flywayVersion = '4.1.1' diff --git a/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt b/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt index 65b7ce6..e24aa9f 100644 --- a/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt +++ b/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt @@ -5,10 +5,8 @@ import com.myapp.domain.UserDetailsImpl import org.slf4j.LoggerFactory import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional @Service -@Transactional class SecurityContextServiceImpl : SecurityContextService { @Suppress("unused") From 541e445180d358e80e3e80d9d013b60406f2a30e Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Sun, 19 Mar 2017 16:30:47 +0700 Subject: [PATCH 15/17] upgraded spring boot and kotlin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4e5c513..0976234 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ import javax.xml.bind.JAXB buildscript { ext { - kotlinVersion = '1.1.0' - springBootVersion = '1.5.1.RELEASE' + kotlinVersion = '1.1.1' + springBootVersion = '1.5.2.RELEASE' jooqVersion = '3.9.0' flywayVersion = '4.1.1' h2Version = '1.4.193' From f63dafc8f5f65af7284a1bfc65a08dec082a8e1b Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Sun, 19 Mar 2017 16:46:59 +0700 Subject: [PATCH 16/17] jooq 3.9.1 --- build.gradle | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index 0976234..d202afc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,11 @@ -import groovy.xml.MarkupBuilder import org.jooq.util.GenerationTool - -import javax.xml.bind.JAXB +import org.jooq.util.jaxb.* buildscript { ext { kotlinVersion = '1.1.1' springBootVersion = '1.5.2.RELEASE' - jooqVersion = '3.9.0' + jooqVersion = '3.9.1' flywayVersion = '4.1.1' h2Version = '1.4.193' swaggerVersion = '2.6.1' @@ -77,31 +75,24 @@ flyway { task jooqGenerate { doLast { - def writer = new StringWriter() - new MarkupBuilder(writer) - .configuration('xmlns': "http://www.jooq.org/xsd/jooq-codegen-${jooqVersion}.xsd") { - jdbc { - driver 'org.h2.Driver' - url 'jdbc:h2:./db/tmp;MODE=MySQL' - user 'sa' - password '' - } - generator { - database { - inputSchema 'public' - outputSchemaToDefault 'true' - } - generate { - } - target { - packageName 'com.myapp.generated' - directory 'src/main/java' - } - } - } - GenerationTool.generate( - JAXB.unmarshal(new StringReader(writer.toString()), org.jooq.util.jaxb.Configuration.class) - ) + Configuration configuration = new Configuration() + .withJdbc(new Jdbc() + .withDriver("org.h2.Driver") + .withUrl("jdbc:h2:./db/tmp;MODE=MySQL") + .withUser("sa") + .withPassword("") + ) + .withGenerator(new Generator() + .withDatabase(new Database() + .withInputSchema("public") + .withOutputSchemaToDefault(true) + ) + .withTarget(new Target() + .withPackageName("com.myapp.generated") + .withDirectory("src/main/java") + ) + ) + GenerationTool.generate(configuration) } } jooqGenerate.dependsOn flywayMigrate From c6daf1df3dcb06813bbeb9096e5a5672c58481a7 Mon Sep 17 00:00:00 2001 From: Akira Sosa Date: Sun, 19 Mar 2017 17:00:56 +0700 Subject: [PATCH 17/17] updated dependencies --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index d202afc..5dcbd1e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,8 @@ buildscript { kotlinVersion = '1.1.1' springBootVersion = '1.5.2.RELEASE' jooqVersion = '3.9.1' - flywayVersion = '4.1.1' - h2Version = '1.4.193' + flywayVersion = '4.1.2' + h2Version = '1.4.194' swaggerVersion = '2.6.1' } repositories { @@ -50,21 +50,21 @@ dependencies { compile group: 'org.springframework.boot', name: 'spring-boot-devtools' compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: kotlinVersion compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlinVersion - compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.8.6' + compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.8.7' compile group: 'org.jooq', name: 'jooq', version: jooqVersion compile group: 'org.flywaydb', name: 'flyway-core', version: flywayVersion compile group: 'com.zaxxer', name: 'HikariCP' compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0' compile group: 'io.springfox', name: 'springfox-swagger2', version: swaggerVersion compile group: 'io.springfox', name: 'springfox-swagger-ui', version: swaggerVersion - compile group: 'com.github.ulisesbocchio', name: 'jasypt-spring-boot-starter', version: '1.11' + compile group: 'com.github.ulisesbocchio', name: 'jasypt-spring-boot-starter', version: '1.12' runtime group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '1.5.8' runtime group: 'com.h2database', name: 'h2', version: h2Version testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test' testCompile group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit', version: kotlinVersion testCompile group: 'org.assertj', name: 'assertj-core', version: '3.6.2' testCompile group: 'com.nhaarman', name: 'mockito-kotlin', version: '1.1.0' - testCompile group: 'com.beust', name: 'klaxon', version: '0.27' + testCompile group: 'com.beust', name: 'klaxon', version: '0.31' } flyway {