diff --git a/.gitignore b/.gitignore index a125cfa..ddb5851 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..58d73fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ install: - pip install awscli - aws --version -script: ./gradlew clean build jacocoTestReport coveralls +script: + - ./gradlew clean jooqGenerate build before_deploy: # Parse branch name and determine an environment to deploy diff --git a/README.md b/README.md index 1e61790..a7175b8 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,25 @@ 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 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? + * A) Use IntelliJ IDEA 2017.1 and run or debug Application.kt. ## 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 +68,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..5dcbd1e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,79 +1,99 @@ +import org.jooq.util.GenerationTool +import org.jooq.util.jaxb.* + buildscript { ext { - springBootVersion = '1.5.1.RELEASE' - querydslVersion = '4.1.4' + kotlinVersion = '1.1.1' + springBootVersion = '1.5.2.RELEASE' + jooqVersion = '3.9.1' + flywayVersion = '4.1.2' + h2Version = '1.4.194' swaggerVersion = '2.6.1' - spockVersion = '1.1-groovy-2.4-rc-3' } repositories { jcenter() maven { url "https://plugins.gradle.org/m2/" } } 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}" } } -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() } + 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.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: '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.12' 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.31' +} + +flyway { + url = 'jdbc:h2:./db/tmp;MODE=MySQL' + user = 'sa' + password = '' } -jacocoTestReport { - reports { - xml.enabled = true // coveralls plugin depends on xml format report - html.enabled = true +task jooqGenerate { + doLast { + 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 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 af6d662..ca78035 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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..e24aa9f --- /dev/null +++ b/src/main/kotlin/com/myapp/auth/SecurityContextServiceImpl.kt @@ -0,0 +1,28 @@ +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 + +@Service +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/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; 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 +) +