diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6885c1a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.9" + +services: + mysql: + image: mysql:8.0 + container_name: mysql-db + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: mydb + MYSQL_USER: user + MYSQL_PASSWORD: user123 + ports: + - "3306:3306" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9eaea54..07efe5a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,107 +1,113 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - pl.milosnicyIT - code-warehouse-backend - 0.0.1-SNAPSHOT - code-warehouse-backend - code-warehouse-backend - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-webmvc - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + pl.milosnicyIT + code-warehouse-backend + 0.0.1-SNAPSHOT + code-warehouse-backend + code-warehouse-backend + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-webmvc + - - com.h2database - h2 - runtime - - - com.mysql - mysql-connector-j - runtime - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-data-jpa-test - test - - - org.springframework.boot - spring-boot-starter-security-test - test - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - + + com.h2database + h2 + runtime + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-data-jpa-test + test + + + org.springframework.boot + spring-boot-starter-security-test + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + io.jsonwebtoken + jjwt + 0.12.6 + + - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + diff --git a/src/main/java/org/commons/login/Password.java b/src/main/java/org/commons/login/Password.java new file mode 100644 index 0000000..7d34ee6 --- /dev/null +++ b/src/main/java/org/commons/login/Password.java @@ -0,0 +1,34 @@ +package org.commons.login; + +import lombok.NonNull; + +import java.util.Objects; + +public class Password { + private final String password; + + public Password(@NonNull final String password) { + if (password.length() < 15) throw new IllegalArgumentException("Password must have at least 15 characters"); + if (password.contains(" ")) throw new IllegalArgumentException("Password must not contain spaces"); + + this.password = password; + } + + @Override + public String toString() { + return this.password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Password password1 = (Password) o; + return Objects.equals(password, password1.password); + } + + @Override + public int hashCode() { + return Objects.hashCode(password); + } +} diff --git a/src/main/java/org/commons/login/Username.java b/src/main/java/org/commons/login/Username.java new file mode 100644 index 0000000..bf384ee --- /dev/null +++ b/src/main/java/org/commons/login/Username.java @@ -0,0 +1,34 @@ +package org.commons.login; + +import lombok.NonNull; + +import java.util.Objects; + +public class Username { + private final String username; + + public Username(@NonNull final String username) { + if (username.length() < 3) throw new IllegalArgumentException("Username must have at least 3 characters"); + if (username.contains(" ")) throw new IllegalArgumentException("Username must not contain spaces"); + + this.username = username; + } + + @Override + public String toString() { + return this.username; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Username username1 = (Username) o; + return Objects.equals(username, username1.username); + } + + @Override + public int hashCode() { + return Objects.hashCode(username); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/JWTAuthFilter.java b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/JWTAuthFilter.java new file mode 100644 index 0000000..4235324 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/JWTAuthFilter.java @@ -0,0 +1,57 @@ +package pl.milosnicyit.codewarehousebackend.config.auth; + + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; + +import java.io.IOException; + +@Component +public class JWTAuthFilter extends OncePerRequestFilter { + private final JWTService jwtService; + private final UserDetailsService userDetailsService; + + @Autowired + public JWTAuthFilter(JWTService jwtService, UserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException { + String authorization = request.getHeader("Authorization"); + + if (authorization != null && authorization.startsWith("Bearer ")) { + String token = authorization.substring(7); + try { + String login = this.jwtService.extractLogin(token); + if (login != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(login); + + if (this.jwtService.validateToken(token)) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + } catch (final Exception exception) { + exception.printStackTrace(); + } + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java new file mode 100644 index 0000000..1285db6 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/config/auth/WebSecurityConfig.java @@ -0,0 +1,55 @@ +package pl.milosnicyit.codewarehousebackend.config.auth; + +import jakarta.servlet.http.HttpSession; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static pl.milosnicyit.codewarehousebackend.controllers.v1.RestConstant.PATH; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + private final JWTAuthFilter jwtAuthFilter; + + public WebSecurityConfig(JWTAuthFilter jwtAuthFilter) { + this.jwtAuthFilter = jwtAuthFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSession httpSession) throws Exception { + return http.cors(httpSecurityCorsConfigurer -> { + }) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers(PATH + "auth/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + + //todo change this or delete + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } + +} \ No newline at end of file diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/ControllersConstant.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/ControllersConstant.java new file mode 100644 index 0000000..aaecd64 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/ControllersConstant.java @@ -0,0 +1,5 @@ +package pl.milosnicyit.codewarehousebackend.controllers; + +public final class ControllersConstant { + public static final String API_PATH = "/api"; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/RestConstant.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/RestConstant.java new file mode 100644 index 0000000..505d550 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/RestConstant.java @@ -0,0 +1,7 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1; + +import static pl.milosnicyit.codewarehousebackend.controllers.ControllersConstant.API_PATH; + +public final class RestConstant { + public static final String PATH = API_PATH + "/v1/"; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java new file mode 100644 index 0000000..4de5ffe --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java @@ -0,0 +1,28 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +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; +import pl.milosnicyit.codewarehousebackend.users.UsersService; + +import static pl.milosnicyit.codewarehousebackend.controllers.v1.RestConstant.PATH; + +@RestController +@RequestMapping(PATH + "auth") +public class AuthController { + private final UsersService usersService; + + @Autowired + public AuthController(UsersService usersService) { + this.usersService = usersService; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody UserRequest userRequest) { + final String token = this.usersService.registerUser(userRequest.getUsername(), userRequest.getPassword()); + return ResponseEntity.ok(new TokenResponse(token)); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java new file mode 100644 index 0000000..5995e46 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/TokenResponse.java @@ -0,0 +1,14 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class TokenResponse { + private String token; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/UserRequest.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/UserRequest.java new file mode 100644 index 0000000..a6b8e5f --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/UserRequest.java @@ -0,0 +1,15 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class UserRequest { + private String username; + private String password; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java new file mode 100644 index 0000000..719ee6f --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package pl.milosnicyit.codewarehousebackend.handlers; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception) { + return ResponseEntity.badRequest().body(new HandlerDTO(exception.getMessage())); + } + + @ExceptionHandler(UsernameNotFoundException.class) + public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException exception) { + return ResponseEntity.badRequest().body(new HandlerDTO(exception.getMessage())); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/HandlerDTO.java b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/HandlerDTO.java new file mode 100644 index 0000000..59073ad --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/HandlerDTO.java @@ -0,0 +1,10 @@ +package pl.milosnicyit.codewarehousebackend.handlers; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class HandlerDTO { + private String error; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtService.java new file mode 100644 index 0000000..4f67780 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtService.java @@ -0,0 +1,28 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import lombok.NonNull; +import org.commons.login.Username; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +class AppJwtService implements JWTService { + private final JwtBasicService jwtBasicService; + + public AppJwtService(@NonNull final JWTSecretService jwtSecretService) { + this.jwtBasicService = new JwtBasicService(jwtSecretService); + } + + @Override + public String generateToken(@NonNull String username) { + return this.jwtBasicService.generateToken(new Username(username)); + } + + @Override + public String extractLogin(@NonNull String token) { + return this.jwtBasicService.extractLogin(token).toString(); + } + + @Override + public boolean validateToken(@NonNull String jwtToken) { + return this.jwtBasicService.validateToken(jwtToken); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTService.java new file mode 100644 index 0000000..f86070d --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTService.java @@ -0,0 +1,11 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import lombok.NonNull; + +public interface JWTService { + String generateToken(@NonNull final String username); + + String extractLogin(@NonNull final String token); + + boolean validateToken(@NonNull final String jwtToken); +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceConfiguration.java new file mode 100644 index 0000000..164c196 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceConfiguration.java @@ -0,0 +1,21 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +@Configuration +public class JWTServiceConfiguration { + private final JWTSecretService jwtSecretService; + + @Autowired + public JWTServiceConfiguration(JWTSecretService jwtSecretService) { + this.jwtSecretService = jwtSecretService; + } + + @Bean + public JWTService jwtService() { + return new AppJwtService(this.jwtSecretService); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicService.java new file mode 100644 index 0000000..c584887 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicService.java @@ -0,0 +1,57 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.NonNull; +import org.commons.login.Username; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +class JwtBasicService { + private final JWTSecretService jwtSecretService; + + public JwtBasicService(JWTSecretService jwtSecretService) { + this.jwtSecretService = jwtSecretService; + } + + public String generateToken(@NonNull final Username username) { + return Jwts.builder() + .subject(username.toString()) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + 86400000)) // 1 day + .signWith(getSigningKey()) + .compact(); + } + + public Username extractLogin(@NonNull final String token) { + final String subject = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + + return new Username(subject); + } + + public boolean validateToken(@NonNull final String jwtToken) { + try { + Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(jwtToken); + return true; + } catch (final JwtException | IllegalArgumentException e) { + return false; + } + } + + private SecretKey getSigningKey() { + final byte[] secretBytes = this.jwtSecretService.getSecret().getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(secretBytes); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTDevService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTDevService.java new file mode 100644 index 0000000..806b2bf --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTDevService.java @@ -0,0 +1,22 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Keys; + +import java.security.Key; + +class JWTDevService implements JWTSecretService { + private final JWTSecret jwtSecret; + + public JWTDevService() { + Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); + String base64Key = Encoders.BASE64.encode(secretKey.getEncoded()); + this.jwtSecret = new JWTSecret(base64Key); + } + + @Override + public String getSecret() { + return this.jwtSecret.toString(); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecret.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecret.java new file mode 100644 index 0000000..d5b0b23 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecret.java @@ -0,0 +1,34 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +import lombok.NonNull; + +import java.util.Objects; + +class JWTSecret { + private final String secret; + + public JWTSecret(@NonNull final String secret) { + if (secret.length() < 15) throw new IllegalArgumentException("Secret must be at least 15 characters"); + if (secret.contains(" ")) throw new IllegalArgumentException("Secret must not contain spaces"); + + this.secret = secret; + } + + @Override + public String toString() { + return this.secret; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JWTSecret jwtSecret = (JWTSecret) o; + return Objects.equals(secret, jwtSecret.secret); + } + + @Override + public int hashCode() { + return Objects.hashCode(secret); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretConfiguration.java new file mode 100644 index 0000000..e2b1fbd --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretConfiguration.java @@ -0,0 +1,13 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JWTSecretConfiguration { + + @Bean + public JWTSecretService jwtSecretService() { + return new JWTDevService(); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretService.java new file mode 100644 index 0000000..bdcb548 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretService.java @@ -0,0 +1,5 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +public interface JWTSecretService { + String getSecret(); +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicService.java new file mode 100644 index 0000000..a4027dd --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicService.java @@ -0,0 +1,20 @@ +package pl.milosnicyit.codewarehousebackend.password; + +import org.commons.login.Password; +import org.springframework.security.crypto.password.PasswordEncoder; + +class PasswordEncoderBasicService implements PasswordEncoderService { + private final PasswordEncoder passwordEncoder; + + public PasswordEncoderBasicService(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + public String encode(Password rawPassword) { + return this.passwordEncoder.encode(rawPassword.toString()); + } + + public boolean matches(Password rawPassword, String encodedPassword) { + return this.passwordEncoder.matches(rawPassword.toString(), encodedPassword); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..5c85b82 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderConfiguration.java @@ -0,0 +1,20 @@ +package pl.milosnicyit.codewarehousebackend.password; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + private final PasswordEncoder passwordEncoder; + + public PasswordEncoderConfiguration() { + this.passwordEncoder = new BCryptPasswordEncoder(); + } + + @Bean + public PasswordEncoderService passwordEncoderService() { + return new PasswordEncoderBasicService(passwordEncoder); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderService.java new file mode 100644 index 0000000..1a002be --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderService.java @@ -0,0 +1,9 @@ +package pl.milosnicyit.codewarehousebackend.password; + +import org.commons.login.Password; + +public interface PasswordEncoderService { + String encode(Password rawPassword); + + boolean matches(Password rawPassword, String encodedPassword); +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java new file mode 100644 index 0000000..a1daaab --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java @@ -0,0 +1,36 @@ +package pl.milosnicyit.codewarehousebackend.users; + +import org.commons.login.Password; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; +import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserDTO; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserRepositoryWrapper; + +class UserAppService implements UsersService { + private final UserRepositoryWrapper userRepositoryWrapper; + private final PasswordEncoderService passwordEncoderService; + private final JWTService jwtService; + + public UserAppService(UserRepositoryWrapper userRepositoryWrapper, PasswordEncoderService passwordEncoderService, + JWTService jwtService) { + this.userRepositoryWrapper = userRepositoryWrapper; + this.passwordEncoderService = passwordEncoderService; + this.jwtService = jwtService; + } + + public String registerUser(String username, String rawPassword) { + if (userRepositoryWrapper.existsByUsername(username)) { + return null; + } + + final Password password = new Password(rawPassword); + final UserDTO userDTO = new UserDTO(); + userDTO.setUsername(username); + userDTO.setPassword(this.passwordEncoderService.encode(password)); + + if (this.userRepositoryWrapper.save(userDTO)) { + return this.jwtService.generateToken(username); + } + return null; + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserServiceConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserServiceConfiguration.java new file mode 100644 index 0000000..516548c --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserServiceConfiguration.java @@ -0,0 +1,28 @@ +package pl.milosnicyit.codewarehousebackend.users; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; +import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserRepositoryWrapper; + +@Configuration +public class UserServiceConfiguration { + private final UserRepositoryWrapper userRepositoryWrapper; + private final PasswordEncoderService passwordEncoderService; + private final JWTService jwtService; + + @Autowired + public UserServiceConfiguration(UserRepositoryWrapper userRepositoryWrapper, PasswordEncoderService passwordEncoderService, + JWTService jwtService) { + this.userRepositoryWrapper = userRepositoryWrapper; + this.passwordEncoderService = passwordEncoderService; + this.jwtService = jwtService; + } + + @Bean + public UsersService usersService() { + return new UserAppService(userRepositoryWrapper, passwordEncoderService, jwtService); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java new file mode 100644 index 0000000..82fab57 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java @@ -0,0 +1,5 @@ +package pl.milosnicyit.codewarehousebackend.users; + +public interface UsersService { + String registerUser(String username, String rawPassword); +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UserEntity.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UserEntity.java new file mode 100644 index 0000000..d647167 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UserEntity.java @@ -0,0 +1,21 @@ +package pl.milosnicyit.codewarehousebackend.users.database; + +import jakarta.persistence.*; +import lombok.Data; + +@Data + +@Entity +@Table(name = "users") +public class UserEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private long id; + + @Column(name = "username", nullable = false, unique = true) + private String username; + + @Column(name = "password", nullable = false) + private String password; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UsersRepository.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UsersRepository.java new file mode 100644 index 0000000..9b95e7c --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/UsersRepository.java @@ -0,0 +1,9 @@ +package pl.milosnicyit.codewarehousebackend.users.database; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UsersRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserDTO.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserDTO.java new file mode 100644 index 0000000..88d7f42 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserDTO.java @@ -0,0 +1,14 @@ +package pl.milosnicyit.codewarehousebackend.users.database.wrapper; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserDTO { + private long id; + private String username; + private String password; +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryBasicWrapper.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryBasicWrapper.java new file mode 100644 index 0000000..a77b2d5 --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryBasicWrapper.java @@ -0,0 +1,45 @@ +package pl.milosnicyit.codewarehousebackend.users.database.wrapper; + +import lombok.NonNull; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import pl.milosnicyit.codewarehousebackend.users.database.UserEntity; +import pl.milosnicyit.codewarehousebackend.users.database.UsersRepository; + +class UserRepositoryBasicWrapper implements UserRepositoryWrapper { + private final UsersRepository usersRepository; + + public UserRepositoryBasicWrapper(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + @Override + public boolean save(@NonNull UserDTO user) { + final UserEntity userEntity = new UserEntity(); + + userEntity.setUsername(user.getUsername()); + userEntity.setPassword(user.getPassword()); + + this.usersRepository.save(userEntity); + return true; + } + + @Override + public UserDTO findByUsername(@NonNull String username) { + final UserEntity userEntity = this.usersRepository.findByUsername(username).orElseThrow(() -> + new UsernameNotFoundException("Username " + username + " not found")); + return new UserDTO(userEntity.getId(), userEntity.getUsername(), userEntity.getPassword()); + } + + @Override + public UserDTO findById(@NonNull Long id) { + final UserEntity userEntity = this.usersRepository.findById(id).orElseThrow(() -> + new UsernameNotFoundException("Username not found")); + return new UserDTO(userEntity.getId(), userEntity.getUsername(), userEntity.getPassword()); + } + + @Override + public boolean existsByUsername(@NonNull String username) { + final UserEntity userEntity = this.usersRepository.findByUsername(username).orElse(null); + return userEntity != null; + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryConfiguration.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryConfiguration.java new file mode 100644 index 0000000..15870bb --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryConfiguration.java @@ -0,0 +1,21 @@ +package pl.milosnicyit.codewarehousebackend.users.database.wrapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import pl.milosnicyit.codewarehousebackend.users.database.UsersRepository; + +@Configuration +public class UserRepositoryConfiguration { + private final UsersRepository usersRepository; + + @Autowired + public UserRepositoryConfiguration(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + @Bean + public UserRepositoryWrapper userRepositoryWrapper() { + return new UserRepositoryBasicWrapper(this.usersRepository); + } +} diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java new file mode 100644 index 0000000..bc80d7a --- /dev/null +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java @@ -0,0 +1,13 @@ +package pl.milosnicyit.codewarehousebackend.users.database.wrapper; + +import lombok.NonNull; + +public interface UserRepositoryWrapper { + boolean save(@NonNull final UserDTO user); + + UserDTO findByUsername(@NonNull final String username); + + UserDTO findById(@NonNull final Long id); + + boolean existsByUsername(@NonNull final String username); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8896c71..3b43a49 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,12 +1,12 @@ spring.application.name=code-warehouse-backend server.port=8082 -spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC -spring.datasource.username=root -spring.datasource.password=your_password +spring.datasource.url=jdbc:mysql://localhost:3306/mydb +spring.datasource.username=user +spring.datasource.password=user123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true diff --git a/src/test/java/org/commons/login/PasswordTest.java b/src/test/java/org/commons/login/PasswordTest.java new file mode 100644 index 0000000..25ba000 --- /dev/null +++ b/src/test/java/org/commons/login/PasswordTest.java @@ -0,0 +1,51 @@ +package org.commons.login; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PasswordTest { + @Test + void createPasswordWhenValid() { + String validPassword = "BardzoSilneHaslo123!"; + + Password password = new Password(validPassword); + + assertEquals(validPassword, password.toString()); + } + + @Test + void shouldCreatePasswordWithExactlyFifteenCharacters() { + String validPassword = "123456789012345"; + + Password password = new Password(validPassword); + + assertEquals(validPassword, password.toString()); + } + + @Test + void shouldThrowExceptionWhenPasswordIsNull() { + assertThrows(NullPointerException.class, () -> new Password(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "haslo", "12345678901234"}) + void shouldThrowExceptionWhenPasswordIsTooShort(String invalidPassword) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new Password(invalidPassword)); + + assertEquals("Password must have at least 15 characters", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"BardzoSilne Haslo123", "1234567890 12345", " ", " spacjaNaPoczatku123"}) + void shouldThrowExceptionWhenPasswordContainsSpaces(String invalidPassword) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new Password(invalidPassword)); + + assertEquals("Password must not contain spaces", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/org/commons/login/UsernameTest.java b/src/test/java/org/commons/login/UsernameTest.java new file mode 100644 index 0000000..fa803bd --- /dev/null +++ b/src/test/java/org/commons/login/UsernameTest.java @@ -0,0 +1,50 @@ +package org.commons.login; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UsernameTest { + @ParameterizedTest + @ValueSource(strings = {"test", "%^#$#S"}) + void shouldCreateUsernameWhenValid(String validName) { + Username username = new Username(validName); + + assertEquals(validName, username.toString()); + } + + @Test + void shouldCreateUsernameWithExactlyThreeCharacters() { + String validName = "jan"; + + Username username = new Username(validName); + + assertEquals(validName, username.toString()); + } + + @Test + void shouldThrowExceptionWhenUsernameIsNull() { + assertThrows(NullPointerException.class, () -> new Username(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "a", "ab", "@"}) + void shouldThrowExceptionWhenUsernameIsTooShort(String invalidName) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new Username(invalidName)); + + assertEquals("Username must have at least 3 characters", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" test", " &*( ", " test ", "test "}) + void shouldThrowExceptionWhenUsernameContainsSpace(String invalidName) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new Username(invalidName)); + + assertEquals("Username must not contain spaces", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java new file mode 100644 index 0000000..c6fb160 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java @@ -0,0 +1,65 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +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; +import static pl.milosnicyit.codewarehousebackend.controllers.v1.RestConstant.PATH; +import static pl.milosnicyit.codewarehousebackend.helpers.JsonHelper.toJson; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class AuthControllerE2ETest { + private static final String REGISTER_ENDPOINT = PATH + "auth/register"; + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldRegisterNewUserEndToEnd() throws Exception { + String uniqueUsername = "e2e_ddffgdfdfdf"; + String password = "realSecretPassword123!"; + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(uniqueUsername, password))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").isNotEmpty()) + .andExpect(jsonPath("$.token").isString()); + } + + @Test + void shouldReturnEmptyTokenWhenUserAlreadyExistsEndToEnd() throws Exception { + String duplicateUsername = "e2e_duplicate_"; + String password = "realSecretPassword123!"; + String jsonRequest = getUserRequestJson(duplicateUsername, password); + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").isNotEmpty()); + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").isEmpty()); + } + + private String getUserRequestJson(String username, String password) throws JsonProcessingException { + UserRequest userRequest = new UserRequest(); + userRequest.setUsername(username); + userRequest.setPassword(password); + return toJson(userRequest); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java new file mode 100644 index 0000000..ab84fa3 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java @@ -0,0 +1,68 @@ +package pl.milosnicyit.codewarehousebackend.controllers.v1.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; +import pl.milosnicyit.codewarehousebackend.users.UsersService; + +import static org.mockito.Mockito.when; +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; +import static pl.milosnicyit.codewarehousebackend.controllers.v1.RestConstant.PATH; +import static pl.milosnicyit.codewarehousebackend.helpers.JsonHelper.toJson; + +@WebMvcTest(AuthController.class) +@AutoConfigureMockMvc(addFilters = false) +class AuthSpecificationTest { + private static final String REGISTER_ENDPOINT = PATH + "auth/register"; + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private UsersService usersService; + + @MockitoBean + private JWTService jwtService; + + @Test + void shouldRegisterUserAndReturnOkWithToken() throws Exception { + String username = "testUser"; + String password = "testPassword"; + String generatedToken = "mocked.jwt.token"; + + when(usersService.registerUser(username, password)).thenReturn(generatedToken); + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").value(generatedToken)); + } + + @Test + void shouldReturnBadRequestWhenRegistrationFails() throws Exception { + + when(usersService.registerUser("zajetyLogin", "testPassword")).thenReturn(null); + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").isEmpty()); + } + + private String getUserRequestJson() throws JsonProcessingException { + UserRequest userRequest = new UserRequest(); + userRequest.setUsername("testUser"); + userRequest.setPassword("testPassword"); + return toJson(userRequest); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandlerTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..e30e07c --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandlerTest.java @@ -0,0 +1,28 @@ +package pl.milosnicyit.codewarehousebackend.handlers; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class GlobalExceptionHandlerTest { + private final GlobalExceptionHandler exceptionHandler = new GlobalExceptionHandler(); + + @Test + void shouldHandleIllegalArgumentExceptionAndReturnBadRequest() { + String expectedMessage = "Nieprawidłowy argument testowy"; + IllegalArgumentException exception = new IllegalArgumentException(expectedMessage); + + ResponseEntity response = exceptionHandler.handleIllegalArgumentException(exception); + + assertNotNull(response, "Response entity should not be null"); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode(), "HTTP status should be 400 BAD REQUEST"); + + HandlerDTO body = response.getBody(); + assertNotNull(body, "Response body should not be null"); + + assertEquals(expectedMessage, body.getError(), "Exception message should be mapped to DTO"); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/helpers/JsonHelper.java b/src/test/java/pl/milosnicyit/codewarehousebackend/helpers/JsonHelper.java new file mode 100644 index 0000000..e03e1cc --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/helpers/JsonHelper.java @@ -0,0 +1,13 @@ +package pl.milosnicyit.codewarehousebackend.helpers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; + +public final class JsonHelper { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static String toJson(@NonNull final Object object) throws JsonProcessingException { + return OBJECT_MAPPER.writeValueAsString(object); + } +} diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtServiceTest.java new file mode 100644 index 0000000..801158b --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/AppJwtServiceTest.java @@ -0,0 +1,74 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import org.commons.login.Username; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AppJwtServiceTest { + + @Mock + private JWTSecretService jwtSecretService; + + @Test + void shouldDelegateGenerateToken() { + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(JwtBasicService.class, + (mock, context) -> { + when(mock.generateToken(any(Username.class))).thenReturn("mocked_token"); + })) { + + AppJwtService appJwtService = new AppJwtService(jwtSecretService); + String inputUsername = "testUser"; + + String result = appJwtService.generateToken(inputUsername); + + assertEquals("mocked_token", result); + + JwtBasicService createdMock = mockedConstruction.constructed().get(0); + verify(createdMock).generateToken(Mockito.argThat(arg -> arg.toString().equals(inputUsername))); + } + } + + @Test + void shouldDelegateExtractLogin() { + // given + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(JwtBasicService.class, + (mock, context) -> { + when(mock.extractLogin("some_token")).thenReturn(new Username("extractedUser")); + })) { + + AppJwtService appJwtService = new AppJwtService(jwtSecretService); + + String result = appJwtService.extractLogin("some_token"); + + assertEquals("extractedUser", result); + } + } + + @Test + void shouldDelegateValidateToken() { + // given + try (MockedConstruction mockedConstruction = Mockito.mockConstruction(JwtBasicService.class, + (mock, context) -> { + when(mock.validateToken("valid_token")).thenReturn(true); + })) { + + AppJwtService appJwtService = new AppJwtService(jwtSecretService); + + boolean result = appJwtService.validateToken("valid_token"); + + assertTrue(result); + } + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceSpecificationTest.java new file mode 100644 index 0000000..1e42ce8 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JWTServiceSpecificationTest.java @@ -0,0 +1,27 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(MockitoExtension.class) +class JWTServiceSpecificationTest { + + @Mock + private JWTSecretService jwtSecretService; + + @Test + void shouldCreateJwtServiceBean() { + JWTServiceConfiguration configuration = new JWTServiceConfiguration(jwtSecretService); + + JWTService jwtService = configuration.jwtService(); + + assertNotNull(jwtService, "JWTService bean should not be null"); + assertInstanceOf(AppJwtService.class, jwtService, "Bean should be an instance of AppJwtService"); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicServiceTest.java new file mode 100644 index 0000000..6e29996 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/JwtBasicServiceTest.java @@ -0,0 +1,98 @@ +package pl.milosnicyit.codewarehousebackend.jwt; + +import org.commons.login.Username; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.jwt.secret.JWTSecretService; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JwtBasicServiceTest { + private static final String MOCK_SECRET = "ToJestBardzoTajnyKluczTestowyKtoryMaMin32Znaki!!"; + private static final String TEST_USERNAME = "testUser123"; + + @Mock + private JWTSecretService jwtSecretService; + + private JwtBasicService jwtBasicService; + + @BeforeEach + void setUp() { + when(jwtSecretService.getSecret()).thenReturn(MOCK_SECRET); + jwtBasicService = new JwtBasicService(jwtSecretService); + } + + @Test + void shouldGenerateValidToken() { + Username username = mock(Username.class); + when(username.toString()).thenReturn(TEST_USERNAME); + + String token = jwtBasicService.generateToken(username); + + assertNotNull(token); + assertFalse(token.isEmpty()); + assertEquals(3, token.split("\\.").length); + } + + @Test + void shouldExtractLoginFromToken() { + Username mockUsername = mock(Username.class); + when(mockUsername.toString()).thenReturn(TEST_USERNAME); + + String generatedToken = jwtBasicService.generateToken(mockUsername); + + Username extractedUsername = jwtBasicService.extractLogin(generatedToken); + + assertNotNull(extractedUsername); + assertEquals(TEST_USERNAME, extractedUsername.toString()); + } + + @Test + void shouldReturnTrueWhenTokenIsValid() { + Username mockUsername = mock(Username.class); + when(mockUsername.toString()).thenReturn(TEST_USERNAME); + String generatedToken = jwtBasicService.generateToken(mockUsername); + + boolean isValid = jwtBasicService.validateToken(generatedToken); + + assertTrue(isValid); + } + + @Test + void shouldReturnFalseWhenTokenIsTampered() { + // given + Username mockUsername = mock(Username.class); + when(mockUsername.toString()).thenReturn(TEST_USERNAME); + String generatedToken = jwtBasicService.generateToken(mockUsername); + + String tamperedToken = generatedToken.substring(0, generatedToken.length() - 3) + "BAD"; + + boolean isValid = jwtBasicService.validateToken(tamperedToken); + + assertFalse(isValid); + } + + @Test + void shouldReturnFalseForMalformedString() { + String invalidToken = "to.nie.jest.prawdziwy.token"; + + boolean isValid = jwtBasicService.validateToken(invalidToken); + + assertFalse(isValid); + } + + @Test + void shouldReturnFalseForEmptyToken() { + String emptyToken = ""; + + boolean isValid = jwtBasicService.validateToken(emptyToken); + + assertFalse(isValid); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JETSecretSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JETSecretSpecificationTest.java new file mode 100644 index 0000000..ed2228b --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JETSecretSpecificationTest.java @@ -0,0 +1,16 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class JETSecretSpecificationTest { + @Test + void shouldReturnJWTDevService() { + JWTSecretConfiguration configuration = new JWTSecretConfiguration(); + + JWTSecretService service = configuration.jwtSecretService(); + + assertInstanceOf(JWTDevService.class, service); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretTest.java new file mode 100644 index 0000000..09d282d --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/jwt/secret/JWTSecretTest.java @@ -0,0 +1,51 @@ +package pl.milosnicyit.codewarehousebackend.jwt.secret; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JWTSecretTest { + @Test + void shouldCreateJWTSecretWhenValid() { + String validSecret = "BardzoTajnySekretJWT123!@#"; + + JWTSecret jwtSecret = new JWTSecret(validSecret); + + assertEquals(validSecret, jwtSecret.toString()); + } + + @Test + void shouldCreateJWTSecretWithExactlyFifteenCharacters() { + String validSecret = "123456789012345"; + + JWTSecret jwtSecret = new JWTSecret(validSecret); + + assertEquals(validSecret, jwtSecret.toString()); + } + + @Test + void shouldThrowExceptionWhenSecretIsNull() { + assertThrows(NullPointerException.class, () -> new JWTSecret(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "tajny", "12345678901234"}) + void shouldThrowExceptionWhenSecretIsTooShort(String invalidSecret) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new JWTSecret(invalidSecret)); + + assertEquals("Secret must be at least 15 characters", exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"Bardzo TajnySekret", "1234567890 12345", " ", " spacjaNaPoczatku1"}) + void shouldThrowExceptionWhenSecretContainsSpaces(String invalidSecret) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new JWTSecret(invalidSecret)); + + assertEquals("Secret must not contain spaces", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java new file mode 100644 index 0000000..2d6211c --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java @@ -0,0 +1,82 @@ +package pl.milosnicyit.codewarehousebackend.password; + +import org.commons.login.Password; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PasswordEncoderBasicServiceTest { + + @Mock + private PasswordEncoder passwordEncoder; + + private PasswordEncoderBasicService passwordEncoderService; + + @BeforeEach + void setUp() { + passwordEncoderService = new PasswordEncoderBasicService(passwordEncoder); + } + + @Test + void shouldEncodePassword() { + // given + String rawPasswordString = "mojeSuperHaslo123"; + String encodedPassword = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; + + Password mockPassword = mock(Password.class); + when(mockPassword.toString()).thenReturn(rawPasswordString); + + when(passwordEncoder.encode(rawPasswordString)).thenReturn(encodedPassword); + + // when + String result = passwordEncoderService.encode(mockPassword); + + // then + assertEquals(encodedPassword, result); + verify(passwordEncoder, times(1)).encode(rawPasswordString); + } + + @Test + void shouldReturnTrueWhenPasswordsMatch() { + // given + String rawPasswordString = "mojeSuperHaslo123"; + String encodedPasswordFromDb = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; + + Password mockPassword = mock(Password.class); + when(mockPassword.toString()).thenReturn(rawPasswordString); + + when(passwordEncoder.matches(rawPasswordString, encodedPasswordFromDb)).thenReturn(true); + + boolean isMatch = passwordEncoderService.matches(mockPassword, encodedPasswordFromDb); + + // then + assertTrue(isMatch); + verify(passwordEncoder, times(1)).matches(rawPasswordString, encodedPasswordFromDb); + } + + @Test + void shouldReturnFalseWhenPasswordsDoNotMatch() { + // given + String rawPasswordString = "zleHaslo"; + String encodedPasswordFromDb = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; + + Password mockPassword = mock(Password.class); + when(mockPassword.toString()).thenReturn(rawPasswordString); + + when(passwordEncoder.matches(rawPasswordString, encodedPasswordFromDb)).thenReturn(false); + + // when + boolean isMatch = passwordEncoderService.matches(mockPassword, encodedPasswordFromDb); + + // then + assertFalse(isMatch); + verify(passwordEncoder, times(1)).matches(rawPasswordString, encodedPasswordFromDb); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderSpecificationTest.java new file mode 100644 index 0000000..08e315a --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderSpecificationTest.java @@ -0,0 +1,20 @@ +package pl.milosnicyit.codewarehousebackend.password; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class PasswordEncoderSpecificationTest { + + @Test + void shouldCreatePasswordEncoderServiceBean() { + PasswordEncoderConfiguration configuration = new PasswordEncoderConfiguration(); + + PasswordEncoderService service = configuration.passwordEncoderService(); + + + assertNotNull(service, "PasswordEncoderService bean should not be null"); + assertInstanceOf(PasswordEncoderBasicService.class, service, "Bean should be an instance of PasswordEncoderBasicService"); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java new file mode 100644 index 0000000..e5da739 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java @@ -0,0 +1,91 @@ +package pl.milosnicyit.codewarehousebackend.users; + +import org.commons.login.Password; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; +import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserDTO; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserRepositoryWrapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserAppServiceTest { + + @Mock + private UserRepositoryWrapper userRepositoryWrapper; + + @Mock + private PasswordEncoderService passwordEncoderService; + + @Mock + private JWTService jwtService; + + @InjectMocks + private UserAppService userAppService; + + @Test + void shouldReturnNullWhenUsernameIsAlreadyTaken() { + String username = "zajetyLogin"; + String rawPassword = "secretPassworddddddddddddddddddddddddddddddddddddd"; + + when(userRepositoryWrapper.existsByUsername(username)).thenReturn(true); + + String result = userAppService.registerUser(username, rawPassword); + + assertNull(result, "Should return null if user already exists"); + + verify(userRepositoryWrapper, never()).save(any()); + verify(jwtService, never()).generateToken(anyString()); + } + + @Test + void shouldRegisterUserAndReturnTokenWhenDataIsCorrect() { + String username = "nowyUser"; + String rawPassword = "secretPasswodddddddddddddddddddddddd"; + String encodedPassword = "encodedPassword123"; + String expectedToken = "jwt.token.here"; + + when(userRepositoryWrapper.existsByUsername(username)).thenReturn(false); + when(passwordEncoderService.encode(any(Password.class))).thenReturn(encodedPassword); + when(userRepositoryWrapper.save(any(UserDTO.class))).thenReturn(true); + when(jwtService.generateToken(username)).thenReturn(expectedToken); + + String result = userAppService.registerUser(username, rawPassword); + + assertEquals(expectedToken, result, "Should return generated JWT token"); + + ArgumentCaptor userDtoCaptor = ArgumentCaptor.forClass(UserDTO.class); + verify(userRepositoryWrapper).save(userDtoCaptor.capture()); + + UserDTO capturedUser = userDtoCaptor.getValue(); + assertEquals(username, capturedUser.getUsername(), "Saved user should have correct username"); + assertEquals(encodedPassword, capturedUser.getPassword(), "Saved user should have ENCODED password"); + } + + @Test + void shouldReturnNullWhenDatabaseSaveFails() { + String username = "nowyUser"; + String rawPassword = "secretPdddddddddddddddddddddddddddddassword"; + String encodedPassword = "encodedPassword123"; + + when(userRepositoryWrapper.existsByUsername(username)).thenReturn(false); + when(passwordEncoderService.encode(any(Password.class))).thenReturn(encodedPassword); + + when(userRepositoryWrapper.save(any(UserDTO.class))).thenReturn(false); + + String result = userAppService.registerUser(username, rawPassword); + + assertNull(result, "Should return null if saving to DB fails"); + + verify(jwtService, never()).generateToken(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserServiceSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserServiceSpecificationTest.java new file mode 100644 index 0000000..0ce0d79 --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserServiceSpecificationTest.java @@ -0,0 +1,39 @@ +package pl.milosnicyit.codewarehousebackend.users; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.milosnicyit.codewarehousebackend.jwt.JWTService; +import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; +import pl.milosnicyit.codewarehousebackend.users.database.wrapper.UserRepositoryWrapper; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(MockitoExtension.class) +class UserServiceSpecificationTest { + + @Mock + private UserRepositoryWrapper userRepositoryWrapper; + + @Mock + private PasswordEncoderService passwordEncoderService; + + @Mock + private JWTService jwtService; + + @Test + void shouldCreateUsersServiceBean() { + UserServiceConfiguration configuration = new UserServiceConfiguration( + userRepositoryWrapper, + passwordEncoderService, + jwtService + ); + + UsersService usersService = configuration.usersService(); + + assertNotNull(usersService, "UsersService bean should not be null"); + assertInstanceOf(UserAppService.class, usersService, "Bean should be an instance of UserAppService"); + } +} \ No newline at end of file