diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 89fb05fe1c1c6f0a9aabe9a034320ee6789dc372..a74e1d6a911805b26a95eb345be549afb55b81ee 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,10 +1,8 @@ -name: Build - +name: SonarQube on: push: branches: - - main - - staging + - '*' pull_request: types: [opened, synchronize, reopened] diff --git a/pom.xml b/pom.xml index 9241721030bf479832618ab802d0c51a37e5e12f..a63ccbe75f2dd4fdd2e922b756d2fa0629cd1ae7 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,8 @@ </scm> <properties> <java.version>21</java.version> + <sonar.organization>safetypin-official</sonar.organization> + <sonar.host.url>https://sonarcloud.io</sonar.host.url> </properties> <dependencyManagement> <dependencies> @@ -74,6 +76,24 @@ <scope>test</scope> </dependency> + <!-- Spring Mail + testing suite --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mail</artifactId> + <version>3.4.2</version> + </dependency> + <dependency> + <groupId>com.icegreen</groupId> + <artifactId>greenmail-junit5</artifactId> + <version>2.1.3</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.activation</groupId> + <artifactId>activation</artifactId> + <version>1.1.1</version> + </dependency> + <!-- JUnit 5 for unit and regression testing --> <dependency> <groupId>org.junit.jupiter</groupId> diff --git a/src/main/java/com/safetypin/authentication/AuthenticationApplication.java b/src/main/java/com/safetypin/authentication/AuthenticationApplication.java index b54a9d5381a33cdf0bc8f447f1dcd01b76f08e7f..4a2663bc70b02db96bfcabb9312ae35a510ae936 100644 --- a/src/main/java/com/safetypin/authentication/AuthenticationApplication.java +++ b/src/main/java/com/safetypin/authentication/AuthenticationApplication.java @@ -8,17 +8,17 @@ import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class AuthenticationApplication { - public static void main(String[] args) { - SpringApplication.run(AuthenticationApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(AuthenticationApplication.class, args); + } } @RestController @RequestMapping("/") class HelloController { - @GetMapping - public String sayHello() { - return "Hello, World!"; - } + @GetMapping + public String sayHello() { + return "Hello, World!"; + } } \ No newline at end of file diff --git a/src/main/java/com/safetypin/authentication/config/AsyncConfig.java b/src/main/java/com/safetypin/authentication/config/AsyncConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a8367a78f00cd7a989e09feb147d673d547c2029 --- /dev/null +++ b/src/main/java/com/safetypin/authentication/config/AsyncConfig.java @@ -0,0 +1,24 @@ +package com.safetypin.authentication.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "emailTaskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("EmailThread-"); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java b/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java index 7ca84622cbdab798b161cdf8726b879bea775321..0869e05fe92245a14d85d5640e5c1d2a91047580 100644 --- a/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java +++ b/src/main/java/com/safetypin/authentication/controller/AuthenticationController.java @@ -4,12 +4,12 @@ import com.safetypin.authentication.dto.*; import com.safetypin.authentication.exception.InvalidCredentialsException; import com.safetypin.authentication.exception.UserAlreadyExistsException; import com.safetypin.authentication.service.AuthenticationService; +import jakarta.validation.Valid; import com.safetypin.authentication.service.GoogleAuthService; import com.safetypin.authentication.service.JwtService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; @RestController @@ -53,13 +53,14 @@ public class AuthenticationController { } + // Endpoint for email login @PostMapping("/login-email") public ResponseEntity<AuthResponse> loginEmail(@RequestParam String email, @RequestParam String password) { try { String jwt = authenticationService.loginUser(email, password); return ResponseEntity.ok(new AuthResponse(true, "OK", new Token(jwt))); - } catch (InvalidCredentialsException e){ + } catch (InvalidCredentialsException e) { AuthResponse response = new AuthResponse(false, e.getMessage(), null); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } diff --git a/src/main/java/com/safetypin/authentication/dto/AuthResponse.java b/src/main/java/com/safetypin/authentication/dto/AuthResponse.java index a29962ff41457e80c4adbbb6f198b35b121751b4..6dab9c4cf1de8eece2643d5c09035a53ac7ba3e3 100644 --- a/src/main/java/com/safetypin/authentication/dto/AuthResponse.java +++ b/src/main/java/com/safetypin/authentication/dto/AuthResponse.java @@ -1,6 +1,9 @@ package com.safetypin.authentication.dto; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; @Data @Getter diff --git a/src/main/java/com/safetypin/authentication/dto/ErrorResponse.java b/src/main/java/com/safetypin/authentication/dto/ErrorResponse.java index e7bb4102d7b8ee0ef4a89fd83cbef9406ccb6922..110cb738155cc93c353937b3947a836ed825894c 100644 --- a/src/main/java/com/safetypin/authentication/dto/ErrorResponse.java +++ b/src/main/java/com/safetypin/authentication/dto/ErrorResponse.java @@ -11,7 +11,7 @@ import java.time.LocalDateTime; @Getter @Setter @NoArgsConstructor -public class ErrorResponse{ +public class ErrorResponse { private int status; private String message; private LocalDateTime timestamp; diff --git a/src/main/java/com/safetypin/authentication/dto/UserResponse.java b/src/main/java/com/safetypin/authentication/dto/UserResponse.java index be8d012012efb11128b24114879a31afb2f697b0..736e217c98e5af268455783892bfd476923d33c5 100644 --- a/src/main/java/com/safetypin/authentication/dto/UserResponse.java +++ b/src/main/java/com/safetypin/authentication/dto/UserResponse.java @@ -1,6 +1,9 @@ package com.safetypin.authentication.dto; -import lombok.*; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; import java.time.LocalDate; import java.util.UUID; diff --git a/src/main/java/com/safetypin/authentication/exception/OTPException.java b/src/main/java/com/safetypin/authentication/exception/OTPException.java new file mode 100644 index 0000000000000000000000000000000000000000..897c8b90c87757f8c4894f68d128bdc355d0cadb --- /dev/null +++ b/src/main/java/com/safetypin/authentication/exception/OTPException.java @@ -0,0 +1,7 @@ +package com.safetypin.authentication.exception; + +public class OTPException extends RuntimeException { + public OTPException(String message) { + super(message); + } +} diff --git a/src/main/java/com/safetypin/authentication/model/Role.java b/src/main/java/com/safetypin/authentication/model/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..38c5972169a47c0d782419cb239a47a62d2294a4 --- /dev/null +++ b/src/main/java/com/safetypin/authentication/model/Role.java @@ -0,0 +1,7 @@ +package com.safetypin.authentication.model; + +public enum Role { + REGISTERED_USER, + PREMIUM_USER, + MODERATOR +} diff --git a/src/main/java/com/safetypin/authentication/model/User.java b/src/main/java/com/safetypin/authentication/model/User.java index 63f738b505d663715566e2da56d35a9180dc2433..7fd5f65f99ed01e7058afb07ad8b8dbb267984c2 100644 --- a/src/main/java/com/safetypin/authentication/model/User.java +++ b/src/main/java/com/safetypin/authentication/model/User.java @@ -14,54 +14,64 @@ import java.util.UUID; @NoArgsConstructor public class User { @Id - @Setter @Getter + @Setter + @Getter @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @Setter @Getter + @Setter + @Getter @Column(nullable = false, unique = true) private String email; // May be null for social login users - @Setter @Getter + @Setter + @Getter + @Column(nullable = false) private String password; - @Setter @Getter + @Setter + @Getter @Column(nullable = false) private String name; @Column(nullable = false) private boolean isVerified = false; - @Setter @Getter - private String role; + @Setter + @Getter + @Enumerated(EnumType.STRING) + private Role role; // New fields - @Setter @Getter + @Setter + @Getter private LocalDate birthdate; - @Setter @Getter + @Setter + @Getter private String provider; // "EMAIL", "GOOGLE", "APPLE" + // Getters and setters public boolean isVerified() { return isVerified; } + public void setVerified(boolean verified) { isVerified = verified; } - public UserResponse generateUserResponse(){ + public UserResponse generateUserResponse() { return UserResponse.builder() .email(email) .id(id) .provider(provider) .birthdate(birthdate) - .role(role) + .role(role != null ? role.name() : null) .name(name) .isVerified(isVerified) .build(); } - } diff --git a/src/main/java/com/safetypin/authentication/repository/UserRepository.java b/src/main/java/com/safetypin/authentication/repository/UserRepository.java index f95b36e46e310152b9104b1b1fb8ef892c4d7597..1370373728acefe890fe282fd6107d353c3dc66c 100644 --- a/src/main/java/com/safetypin/authentication/repository/UserRepository.java +++ b/src/main/java/com/safetypin/authentication/repository/UserRepository.java @@ -1,12 +1,15 @@ package com.safetypin.authentication.repository; +import com.safetypin.authentication.model.Role; +import com.safetypin.authentication.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import com.safetypin.authentication.model.User; import java.util.UUID; @Repository public interface UserRepository extends JpaRepository<User, UUID> { User findByEmail(String email); + + User findByRole(Role role); } diff --git a/src/main/java/com/safetypin/authentication/security/PasswordEncoderConfig.java b/src/main/java/com/safetypin/authentication/security/PasswordEncoderConfig.java index a4e107fa1ac73de8fa0c5389dcca59178bcf043e..1dbb102c31430d88ab9945b7818c182a1a02553e 100644 --- a/src/main/java/com/safetypin/authentication/security/PasswordEncoderConfig.java +++ b/src/main/java/com/safetypin/authentication/security/PasswordEncoderConfig.java @@ -11,4 +11,4 @@ public class PasswordEncoderConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/safetypin/authentication/security/SecurityConfig.java b/src/main/java/com/safetypin/authentication/security/SecurityConfig.java index e4483b6ace1e6990f50540772ee0c5d893e44190..20ab02f50ce8dd3852704388e9dda1f1ec602132 100644 --- a/src/main/java/com/safetypin/authentication/security/SecurityConfig.java +++ b/src/main/java/com/safetypin/authentication/security/SecurityConfig.java @@ -31,4 +31,4 @@ public class SecurityConfig { public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/safetypin/authentication/seeder/DevDataSeeder.java b/src/main/java/com/safetypin/authentication/seeder/DevDataSeeder.java index 9a2f3f97063e8e6d0c8e40f26bfbbf74424f668b..2018c87adba7ba92f8d4c5c454b19c8348e5281a 100644 --- a/src/main/java/com/safetypin/authentication/seeder/DevDataSeeder.java +++ b/src/main/java/com/safetypin/authentication/seeder/DevDataSeeder.java @@ -1,9 +1,8 @@ package com.safetypin.authentication.seeder; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import com.safetypin.authentication.repository.UserRepository; -import static com.safetypin.authentication.service.AuthenticationService.EMAIL_PROVIDER; - import jakarta.annotation.PostConstruct; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.password.PasswordEncoder; @@ -11,6 +10,8 @@ import org.springframework.stereotype.Component; import java.time.LocalDate; +import static com.safetypin.authentication.service.AuthenticationService.EMAIL_PROVIDER; + @Component @Profile({"dev"}) public class DevDataSeeder implements Runnable { @@ -38,7 +39,7 @@ public class DevDataSeeder implements Runnable { user1.setPassword(passwordEncoder.encode("password1")); //NOSONAR user1.setName("User One"); user1.setVerified(true); - user1.setRole("user"); + user1.setRole(Role.REGISTERED_USER); user1.setBirthdate(LocalDate.of(1990, 1, 1)); user1.setProvider(EMAIL_PROVIDER); userRepository.save(user1); @@ -48,7 +49,7 @@ public class DevDataSeeder implements Runnable { user2.setPassword(passwordEncoder.encode("password2")); //NOSONAR user2.setName("User Two"); user2.setVerified(true); - user2.setRole("user"); + user2.setRole(Role.REGISTERED_USER); user2.setBirthdate(LocalDate.of(1991, 2, 2)); user2.setProvider(EMAIL_PROVIDER); userRepository.save(user2); @@ -59,7 +60,7 @@ public class DevDataSeeder implements Runnable { user3.setPassword(passwordEncoder.encode("password3")); //NOSONAR user3.setName("User Three"); user3.setVerified(true); - user3.setRole("user"); + user3.setRole(Role.REGISTERED_USER); user3.setBirthdate(LocalDate.of(1992, 3, 3)); user3.setProvider(EMAIL_PROVIDER); userRepository.save(user3); @@ -69,7 +70,7 @@ public class DevDataSeeder implements Runnable { user4.setPassword(passwordEncoder.encode("password4")); //NOSONAR user4.setName("User Four"); user4.setVerified(true); - user4.setRole("user"); + user4.setRole(Role.REGISTERED_USER); user4.setBirthdate(LocalDate.of(1993, 4, 4)); user4.setProvider(EMAIL_PROVIDER); userRepository.save(user4); @@ -79,10 +80,18 @@ public class DevDataSeeder implements Runnable { user5.setPassword(passwordEncoder.encode("password5")); //NOSONAR user5.setName("User Five"); user5.setVerified(true); - user5.setRole("user"); + user5.setRole(Role.PREMIUM_USER); user5.setBirthdate(LocalDate.of(1994, 5, 5)); user5.setProvider(EMAIL_PROVIDER); userRepository.save(user5); + + User user6 = new User(); + user6.setEmail("user6@example.com"); + user6.setPassword(passwordEncoder.encode("password6")); //NOSONAR + user6.setName("User Six"); + user6.setVerified(true); + user6.setRole(Role.MODERATOR); + } } } diff --git a/src/main/java/com/safetypin/authentication/service/AuthenticationService.java b/src/main/java/com/safetypin/authentication/service/AuthenticationService.java index e351df0cd36866c84b848d21e2238b50b0b327e2..064d215910db7c568e17df7567b26a51c294da1e 100644 --- a/src/main/java/com/safetypin/authentication/service/AuthenticationService.java +++ b/src/main/java/com/safetypin/authentication/service/AuthenticationService.java @@ -3,6 +3,7 @@ package com.safetypin.authentication.service; import com.safetypin.authentication.dto.RegistrationRequest; import com.safetypin.authentication.exception.InvalidCredentialsException; import com.safetypin.authentication.exception.UserAlreadyExistsException; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,7 @@ public class AuthenticationService { user.setPassword(encodedPassword); user.setName(request.getName()); user.setVerified(false); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(request.getBirthdate()); user.setProvider(EMAIL_PROVIDER); user = userService.save(user); diff --git a/src/main/java/com/safetypin/authentication/service/EmailService.java b/src/main/java/com/safetypin/authentication/service/EmailService.java new file mode 100644 index 0000000000000000000000000000000000000000..498cf9de236e2a7843fcb26cbf480fc0458696fb --- /dev/null +++ b/src/main/java/com/safetypin/authentication/service/EmailService.java @@ -0,0 +1,77 @@ +package com.safetypin.authentication.service; + +import jakarta.mail.internet.MimeMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.concurrent.CompletableFuture; + +@Service +public class EmailService { + private static final Logger logger = LoggerFactory.getLogger(EmailService.class); + private static final String SENDER = "noreply@safetyp.in"; + private final JavaMailSender mailSender; + + @Autowired + public EmailService(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + @Async("emailTaskExecutor") + public CompletableFuture<Boolean> sendOTPMail(String to, String otp) { + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8"); + + helper.setFrom(SENDER); + helper.setTo(to); + helper.setSubject("OTP Code for SafetyPin"); + + String htmlContent = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + " <style>" + + " body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }" + + " .container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }" + + " .header { background-color: #4285f4; color: white; padding: 10px; text-align: center; border-radius: 5px 5px 0 0; }" + + " .content { padding: 20px; }" + + " .otp-code { font-size: 24px; font-weight: bold; text-align: center; margin: 20px 0; padding: 10px; background-color: #f0f0f0; border-radius: 4px; letter-spacing: 5px; }" + + " .footer { font-size: 12px; color: #777; text-align: center; margin-top: 20px; }" + + " </style>" + + "</head>" + + "<body>" + + " <div class='container'>" + + " <div class='header'>" + + " <h2>SafetyPin Security</h2>" + + " </div>" + + " <div class='content'>" + + " <p>Hello,</p>" + + " <p>Your one-time verification code is:</p>" + + " <div class='otp-code'>" + otp + "</div>" + + " <p>This code will expire in 2 minutes. Please do not share this code with anyone.</p>" + + " <p>If you didn't request this code, please ignore this email.</p>" + + " </div>" + + " <div class='footer'>" + + " <p>This is an automated message. Please do not reply.</p>" + + " <p>© " + java.time.Year.now().getValue() + " SafetyPin. All rights reserved.</p>" + + " </div>" + + " </div>" + + "</body>" + + "</html>"; + + helper.setText(htmlContent, true); // true indicates HTML content + + mailSender.send(mimeMessage); + return CompletableFuture.completedFuture(true); + } catch (Exception e) { + logger.warn("EmailService.sendOTPMail:: Failed to send mail with error; {}", e.getMessage()); + return CompletableFuture.completedFuture(false); + } + } +} diff --git a/src/main/java/com/safetypin/authentication/service/GoogleAuthService.java b/src/main/java/com/safetypin/authentication/service/GoogleAuthService.java index 0fab5334bcf136c3edd689c5108cab4d8fb3d310..ce8ef7ad860c7fe5af0ffade7017a4dcfd264664 100644 --- a/src/main/java/com/safetypin/authentication/service/GoogleAuthService.java +++ b/src/main/java/com/safetypin/authentication/service/GoogleAuthService.java @@ -15,6 +15,7 @@ import com.safetypin.authentication.dto.GoogleAuthDTO; import com.safetypin.authentication.exception.ApiException; import com.safetypin.authentication.exception.InvalidCredentialsException; import com.safetypin.authentication.exception.UserAlreadyExistsException; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,7 +78,7 @@ public class GoogleAuthService { newUser.setPassword(null); newUser.setProvider(EMAIL_PROVIDER); newUser.setVerified(true); - newUser.setRole("USER"); + newUser.setRole(Role.REGISTERED_USER); newUser.setBirthdate(userBirthdate); User user = userService.save(newUser); diff --git a/src/main/java/com/safetypin/authentication/service/OTPService.java b/src/main/java/com/safetypin/authentication/service/OTPService.java index b338aca55d7c58021ce5e648fa48ce1cf362d8dd..1eaf4d51c0adc253861825fd898dd18fa1a9e8ec 100644 --- a/src/main/java/com/safetypin/authentication/service/OTPService.java +++ b/src/main/java/com/safetypin/authentication/service/OTPService.java @@ -1,31 +1,53 @@ package com.safetypin.authentication.service; +import com.safetypin.authentication.exception.OTPException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; @Service public class OTPService { - private static final long OTP_EXPIRATION_SECONDS = 120; // 2 minutes expiration private static final Logger log = LoggerFactory.getLogger(OTPService.class); + private final EmailService emailService; private final ConcurrentHashMap<String, OTPDetails> otpStorage = new ConcurrentHashMap<>(); private final SecureRandom random = new SecureRandom(); + @Autowired + public OTPService(EmailService emailService) { + this.emailService = emailService; + } + public String generateOTP(String email) { String otp = String.format("%06d", random.nextInt(1000000)); OTPDetails details = new OTPDetails(otp, LocalDateTime.now()); otpStorage.put(email, details); - // Simulate sending OTP via email (in production, integrate with an email service) + + try { + boolean status = emailService.sendOTPMail(email, otp).get(); + if (!status) { + throw new OTPException("Failed to send OTP"); + } + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new OTPException("Failed to send OTP: " + e.getMessage()); + } + log.info("Sending OTP {} to {}", otp, email); return otp; } public boolean verifyOTP(String email, String otp) { + if (otp == null) { + throw new NullPointerException("OTP cannot be null"); + } + OTPDetails details = otpStorage.get(email); if (details == null) { return false; diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index b7c904d50fe81e4261f1b3a6d8da9bc3c7e217cf..c285b8f3a59be337bead1db487bcd7720d6fee81 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,12 +1,9 @@ spring.application.name=authentication - spring.datasource.url=jdbc:postgresql://localhost:5432/be-authentication spring.datasource.username=postgres spring.datasource.password=postgres - spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect - # Hibernate Properties spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b8b6b6876d8b2a41a1fbac05b638f338a7c41039..1f57723b9a70ad73a3d55c4e9401aa7f9b34885a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,15 @@ spring.application.name=authentication spring.profiles.active=${PRODUCTION:dev} + +# Spring Mail +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${MAIL_EMAIL} +spring.mail.password=${MAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true + google.client.id=${GOOGLE_CLIENT_ID:default} google.client.secret=${GOOGLE_CLIENT_SECRET:default} jwt.secret=${JWT_SECRET:biggerboysandstolensweetheartsss} \ No newline at end of file diff --git a/src/test/java/com/safetypin/authentication/AuthenticationApplicationTests.java b/src/test/java/com/safetypin/authentication/AuthenticationApplicationTests.java index a528070135724a4e7a25ed890042dfd3bcf6b5cc..ecb4d4d77cfc758998b5f6d80ff002382cf0eda5 100644 --- a/src/test/java/com/safetypin/authentication/AuthenticationApplicationTests.java +++ b/src/test/java/com/safetypin/authentication/AuthenticationApplicationTests.java @@ -1,13 +1,14 @@ package com.safetypin.authentication; import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; class AuthenticationApplicationTest { - @Test - void testMainDoesNotThrowException() { - // Calling the main method should load the context without throwing an exception. - assertDoesNotThrow(() -> AuthenticationApplication.main(new String[] {})); - } + @Test + void testMainDoesNotThrowException() { + // Calling the main method should load the context without throwing an exception. + assertDoesNotThrow(() -> AuthenticationApplication.main(new String[]{})); + } } diff --git a/src/test/java/com/safetypin/authentication/HelloControllerTest.java b/src/test/java/com/safetypin/authentication/HelloControllerTest.java index cb308d4ee48d94b9afc7856f1d598ef9a30217a3..6b252241af31f1ddb269cd4987e12bbc4557c85e 100644 --- a/src/test/java/com/safetypin/authentication/HelloControllerTest.java +++ b/src/test/java/com/safetypin/authentication/HelloControllerTest.java @@ -1,6 +1,7 @@ package com.safetypin.authentication; import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertEquals; class HelloControllerTest { diff --git a/src/test/java/com/safetypin/authentication/config/AsyncConfigTest.java b/src/test/java/com/safetypin/authentication/config/AsyncConfigTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8a38915dac38fb861ed52428d1ad00ca8d44f79b --- /dev/null +++ b/src/test/java/com/safetypin/authentication/config/AsyncConfigTest.java @@ -0,0 +1,52 @@ +package com.safetypin.authentication.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class AsyncConfigTest { + + @Autowired + private Executor emailTaskExecutor; + + @Test + void testEmailTaskExecutorConfiguration() { + // Verify that emailTaskExecutor is not null + assertNotNull(emailTaskExecutor, "Email task executor should not be null"); + + // Verify that it's a ThreadPoolTaskExecutor instance + assertInstanceOf(ThreadPoolTaskExecutor.class, emailTaskExecutor, "Executor should be an instance of ThreadPoolTaskExecutor"); + + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) emailTaskExecutor; + + // Test core pool size + assertEquals(2, executor.getCorePoolSize(), + "Core pool size should be 2"); + + // Test max pool size + assertEquals(5, executor.getMaxPoolSize(), + "Max pool size should be 5"); + + // Test queue capacity + assertEquals(100, executor.getQueueCapacity(), + "Queue capacity should be 100"); + + // Get the actual ThreadPoolExecutor and verify it's initialized + ThreadPoolExecutor threadPoolExecutor = executor.getThreadPoolExecutor(); + assertNotNull(threadPoolExecutor, "ThreadPoolExecutor should be initialized"); + + // Verify thread prefix naming indirectly through a newly created thread + executor.execute(() -> { + String currentThreadName = Thread.currentThread().getName(); + assertTrue(currentThreadName.startsWith("EmailThread-"), + "Thread name should start with 'EmailThread-'"); + }); + } +} diff --git a/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java b/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java index ab3d5012e63ec79723bc9fe0636065408e97aedc..3002d32c3d68eae6b6713d7fe98bd541c5838547 100644 --- a/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java +++ b/src/test/java/com/safetypin/authentication/controller/AuthenticationControllerTest.java @@ -6,6 +6,7 @@ import com.safetypin.authentication.dto.PasswordResetRequest; import com.safetypin.authentication.dto.RegistrationRequest; import com.safetypin.authentication.dto.UserResponse; import com.safetypin.authentication.exception.InvalidCredentialsException; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.exception.UserAlreadyExistsException; import com.safetypin.authentication.model.User; import com.safetypin.authentication.service.AuthenticationService; @@ -33,7 +34,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(AuthenticationController.class) -@Import({AuthenticationControllerTest.TestConfig.class, AuthenticationControllerTest.TestSecurityConfig.class}) +@Import({AuthenticationControllerTest.TestConfig.class}) class AuthenticationControllerTest { @Autowired @@ -92,7 +93,7 @@ class AuthenticationControllerTest { user.setEmail("email@example.com"); user.setPassword("encodedPassword"); user.setName("Test User"); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(request.getBirthdate()); user.setProvider("EMAIL"); @@ -115,7 +116,7 @@ class AuthenticationControllerTest { user.setPassword("encodedPassword"); user.setName("Test User"); user.setVerified(true); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(20)); user.setProvider("EMAIL"); @@ -176,6 +177,17 @@ class AuthenticationControllerTest { .andExpect(jsonPath("$.message").value("OTP verification failed")); } + @Test + void testVerifyOTP_InvalidCredentials() throws Exception { + String errorMessage = "Invalid email or OTP"; + Mockito.when(authenticationService.verifyOTP("email@example.com", "invalid")) + .thenThrow(new InvalidCredentialsException(errorMessage)); + + mockMvc.perform(post("/api/auth/verify-otp") + .param("email", "Invalid OTP code or expired")) + .andExpect(status().isBadRequest()); + } + @Test void testForgotPassword() throws Exception { PasswordResetRequest request = new PasswordResetRequest(); diff --git a/src/test/java/com/safetypin/authentication/dto/ErrorResponseTest.java b/src/test/java/com/safetypin/authentication/dto/ErrorResponseTest.java index 1398d5a2527655c91f052f4ca71b62f1a3003f55..a5249ce2e9c1050524682f320ecfb1b8f8338ec3 100644 --- a/src/test/java/com/safetypin/authentication/dto/ErrorResponseTest.java +++ b/src/test/java/com/safetypin/authentication/dto/ErrorResponseTest.java @@ -1,18 +1,85 @@ package com.safetypin.authentication.dto; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; import java.time.LocalDateTime; +import static org.junit.jupiter.api.Assertions.*; class ErrorResponseTest { @Test - void testErrorResponseConstructor() { - ErrorResponse errorResponse = new ErrorResponse(404, "Resource not found"); + void testNoArgsConstructor() { + // Act + ErrorResponse response = new ErrorResponse(); - assertThat(errorResponse.getStatus()).isEqualTo(404); - assertThat(errorResponse.getMessage()).isEqualTo("Resource not found"); - assertThat(errorResponse.getTimestamp()).isNotNull(); - assertThat(errorResponse.getTimestamp()).isBeforeOrEqualTo(LocalDateTime.now()); + // Assert + assertNull(response.getMessage()); + assertEquals(0, response.getStatus()); + assertNull(response.getTimestamp()); + } + + @Test + void testParameterizedConstructor() { + // Arrange + int status = 404; + String message = "Not Found"; + + // Act + ErrorResponse response = new ErrorResponse(status, message); + LocalDateTime beforeTest = LocalDateTime.now().minusSeconds(1); + + // Assert + assertEquals(status, response.getStatus()); + assertEquals(message, response.getMessage()); + assertNotNull(response.getTimestamp()); + // Check that timestamp is recent + assertTrue(response.getTimestamp().isAfter(beforeTest)); + } + + @Test + void testGettersAndSetters() { + // Arrange + ErrorResponse response = new ErrorResponse(); + int status = 500; + String message = "Internal Server Error"; + LocalDateTime timestamp = LocalDateTime.now(); + + // Act + response.setStatus(status); + response.setMessage(message); + response.setTimestamp(timestamp); + + // Assert + assertEquals(status, response.getStatus()); + assertEquals(message, response.getMessage()); + assertEquals(timestamp, response.getTimestamp()); + } + + @Test + void testEqualsAndHashCode() { + // Arrange + ErrorResponse response1 = new ErrorResponse(404, "Not Found"); + ErrorResponse response2 = new ErrorResponse(404, "Not Found"); + response2.setTimestamp(response1.getTimestamp()); // Ensure same timestamp for equality check + ErrorResponse response3 = new ErrorResponse(500, "Error"); + + // Assert + assertEquals(response1, response2); + assertNotEquals(response1, response3); + assertEquals(response1.hashCode(), response2.hashCode()); + assertNotEquals(response1.hashCode(), response3.hashCode()); + } + + @Test + void testToString() { + // Arrange + ErrorResponse response = new ErrorResponse(404, "Not Found"); + + // Act + String toStringResult = response.toString(); + + // Assert + assertTrue(toStringResult.contains("404")); + assertTrue(toStringResult.contains("Not Found")); + assertTrue(toStringResult.contains("timestamp")); } } diff --git a/src/test/java/com/safetypin/authentication/dto/GoogleAuthDTOTest.java b/src/test/java/com/safetypin/authentication/dto/GoogleAuthDTOTest.java index e7ec48fccb80385eb27e1a00eb67320aedbc11b8..c9d0095f4bed6d5bba42c7710e285122dc2c5933 100644 --- a/src/test/java/com/safetypin/authentication/dto/GoogleAuthDTOTest.java +++ b/src/test/java/com/safetypin/authentication/dto/GoogleAuthDTOTest.java @@ -122,7 +122,7 @@ class GoogleAuthDTOTest { assertEquals(dto1, dto2, "Identical DTOs should be equal"); assertNotEquals(dto1, dto3, "Different DTOs should not be equal"); assertNotEquals(null, dto1, "Should not be equal to null"); - assertNotEquals(dto1, new Object(), "Should not be equal to different object type"); + assertNotEquals(new Object(), dto1, "Should not be equal to different object type"); // Test hashCode assertEquals(dto1.hashCode(), dto2.hashCode(), "Identical DTOs should have same hashCode"); diff --git a/src/test/java/com/safetypin/authentication/dto/PasswordResetRequestTest.java b/src/test/java/com/safetypin/authentication/dto/PasswordResetRequestTest.java index 013ec5dcdbecc804e717e1185c093433603614a8..9447b1c8b6201650a95f657c22cb0cb7d2bf9539 100644 --- a/src/test/java/com/safetypin/authentication/dto/PasswordResetRequestTest.java +++ b/src/test/java/com/safetypin/authentication/dto/PasswordResetRequestTest.java @@ -1,37 +1,20 @@ package com.safetypin.authentication.dto; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import jakarta.validation.ConstraintViolation; -import java.util.Set; +import static org.junit.jupiter.api.Assertions.*; class PasswordResetRequestTest { - private final Validator validator; - - public PasswordResetRequestTest() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - this.validator = factory.getValidator(); - } - @Test - void testPasswordResetRequestValid() { + void testGettersAndSetters() { + // Arrange PasswordResetRequest request = new PasswordResetRequest(); - request.setEmail("user@example.com"); + String email = "test@example.com"; - Set<ConstraintViolation<PasswordResetRequest>> violations = validator.validate(request); - assertThat(violations).isEmpty(); - } - - @Test - void testPasswordResetRequestInvalidEmail() { - PasswordResetRequest request = new PasswordResetRequest(); - request.setEmail("invalid-email"); + // Act + request.setEmail(email); - Set<ConstraintViolation<PasswordResetRequest>> violations = validator.validate(request); - assertThat(violations).isNotEmpty(); + // Assert + assertEquals(email, request.getEmail()); } } diff --git a/src/test/java/com/safetypin/authentication/dto/RegistrationRequestTest.java b/src/test/java/com/safetypin/authentication/dto/RegistrationRequestTest.java index b231e6732dc2eba209553a509fac64f1b35e3120..02a7fc7c38617a7e814e645bea2921730484270a 100644 --- a/src/test/java/com/safetypin/authentication/dto/RegistrationRequestTest.java +++ b/src/test/java/com/safetypin/authentication/dto/RegistrationRequestTest.java @@ -1,42 +1,30 @@ package com.safetypin.authentication.dto; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import jakarta.validation.ConstraintViolation; import java.time.LocalDate; -import java.util.Set; +import static org.junit.jupiter.api.Assertions.*; class RegistrationRequestTest { - private final Validator validator; - - public RegistrationRequestTest() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - this.validator = factory.getValidator(); - } - @Test - void testRegistrationRequestValid() { + void testGettersAndSetters() { + // Arrange RegistrationRequest request = new RegistrationRequest(); - request.setEmail("user@example.com"); - request.setPassword("securePassword"); - request.setName("John Doe"); - request.setBirthdate(LocalDate.of(1995, 5, 10)); - - Set<ConstraintViolation<RegistrationRequest>> violations = validator.validate(request); - assertThat(violations).isEmpty(); - } - - @Test - void testRegistrationRequestMissingFields() { - RegistrationRequest request = new RegistrationRequest(); // Missing required fields - - Set<ConstraintViolation<RegistrationRequest>> violations = validator.validate(request); - assertThat(violations) - .isNotEmpty() - .hasSize(4); // Email, password, name, and birthdate should all be invalid + String email = "test@example.com"; + String password = "Password123"; + String name = "Test User"; + LocalDate birthdate = LocalDate.of(1990, 1, 1); + + // Act + request.setEmail(email); + request.setPassword(password); + request.setName(name); + request.setBirthdate(birthdate); + + // Assert + assertEquals(email, request.getEmail()); + assertEquals(password, request.getPassword()); + assertEquals(name, request.getName()); + assertEquals(birthdate, request.getBirthdate()); } } diff --git a/src/test/java/com/safetypin/authentication/dto/SocialLoginRequestTest.java b/src/test/java/com/safetypin/authentication/dto/SocialLoginRequestTest.java index fe4a2a68ff908394936cc3f3b187054577c34d24..ca7759815416a3b3edbe4f7e72f025adaa73a459 100644 --- a/src/test/java/com/safetypin/authentication/dto/SocialLoginRequestTest.java +++ b/src/test/java/com/safetypin/authentication/dto/SocialLoginRequestTest.java @@ -1,44 +1,36 @@ package com.safetypin.authentication.dto; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import jakarta.validation.ConstraintViolation; import java.time.LocalDate; -import java.util.Set; +import static org.junit.jupiter.api.Assertions.*; class SocialLoginRequestTest { - private final Validator validator; - - public SocialLoginRequestTest() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - this.validator = factory.getValidator(); - } - @Test - void testSocialLoginRequestValid() { + void testGettersAndSetters() { + // Arrange SocialLoginRequest request = new SocialLoginRequest(); - request.setProvider("GOOGLE"); - request.setSocialToken("validToken"); - request.setEmail("socialuser@example.com"); - request.setName("Social User"); - request.setBirthdate(LocalDate.of(2000, 1, 1)); - request.setSocialId("123456789"); - - Set<ConstraintViolation<SocialLoginRequest>> violations = validator.validate(request); - assertThat(violations).isEmpty(); - } - - @Test - void testSocialLoginRequestMissingFields() { - SocialLoginRequest request = new SocialLoginRequest(); // Missing required fields - - Set<ConstraintViolation<SocialLoginRequest>> violations = validator.validate(request); - assertThat(violations) - .isNotEmpty() - .hasSize(6); // All fields should be invalid + String provider = "GOOGLE"; + String socialToken = "token123"; + String email = "test@example.com"; + String name = "Test User"; + LocalDate birthdate = LocalDate.of(1990, 1, 1); + String socialId = "social123"; + + // Act + request.setProvider(provider); + request.setSocialToken(socialToken); + request.setEmail(email); + request.setName(name); + request.setBirthdate(birthdate); + request.setSocialId(socialId); + + // Assert + assertEquals(provider, request.getProvider()); + assertEquals(socialToken, request.getSocialToken()); + assertEquals(email, request.getEmail()); + assertEquals(name, request.getName()); + assertEquals(birthdate, request.getBirthdate()); + assertEquals(socialId, request.getSocialId()); } } diff --git a/src/test/java/com/safetypin/authentication/model/RoleTest.java b/src/test/java/com/safetypin/authentication/model/RoleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..007deeb7922beef9025c72ec019a88f69d4b82b2 --- /dev/null +++ b/src/test/java/com/safetypin/authentication/model/RoleTest.java @@ -0,0 +1,41 @@ +package com.safetypin.authentication.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RoleTest { + + @Test + void testEnumValues() { + // Test that the enum has the expected values + assertEquals(3, Role.values().length); + assertEquals(Role.REGISTERED_USER, Role.valueOf("REGISTERED_USER")); + assertEquals(Role.PREMIUM_USER, Role.valueOf("PREMIUM_USER")); + assertEquals(Role.MODERATOR, Role.valueOf("MODERATOR")); + } + + @Test + void testEnumOrdinals() { + // Test the ordinals (mostly for coverage) + assertEquals(0, Role.REGISTERED_USER.ordinal()); + assertEquals(1, Role.PREMIUM_USER.ordinal()); + assertEquals(2, Role.MODERATOR.ordinal()); + } + + @Test + void testEnumToString() { + // Test the toString method + assertEquals("REGISTERED_USER", Role.REGISTERED_USER.toString()); + assertEquals("PREMIUM_USER", Role.PREMIUM_USER.toString()); + assertEquals("MODERATOR", Role.MODERATOR.toString()); + } + + @Test + void testEnumValueOf() { + // Test the valueOf method + assertEquals(Role.REGISTERED_USER, Role.valueOf("REGISTERED_USER")); + assertEquals(Role.PREMIUM_USER, Role.valueOf("PREMIUM_USER")); + assertEquals(Role.MODERATOR, Role.valueOf("MODERATOR")); + } +} diff --git a/src/test/java/com/safetypin/authentication/model/UserTest.java b/src/test/java/com/safetypin/authentication/model/UserTest.java index 43dc6c4913014c982caa458567bc527047c49cef..cfc7b2a3711b22d2ecbe63e87b40e355072e60c9 100644 --- a/src/test/java/com/safetypin/authentication/model/UserTest.java +++ b/src/test/java/com/safetypin/authentication/model/UserTest.java @@ -1,6 +1,5 @@ package com.safetypin.authentication.model; -import com.safetypin.authentication.dto.UserResponse; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -32,7 +31,7 @@ class UserTest { String password = "secret"; String name = "Test User"; boolean verified = true; - String role = "ADMIN"; + Role role = Role.REGISTERED_USER; LocalDate birthdate = LocalDate.of(2000, 1, 1); String provider = "GOOGLE"; @@ -61,7 +60,7 @@ class UserTest { String password = "password123"; String name = "Another User"; boolean verified = false; - String role = "USER"; + Role role = Role.MODERATOR; LocalDate birthdate = LocalDate.of(1995, 5, 15); String provider = "EMAIL"; @@ -88,36 +87,110 @@ class UserTest { @Test void testGenerateUserResponse() { - // Setup User user = new User(); - UUID id = UUID.randomUUID(); - String email = "test@example.com"; - String password = "secret"; // This shouldn't be in the response - String name = "Test User"; - boolean verified = true; - String role = "ADMIN"; - LocalDate birthdate = LocalDate.of(2000, 1, 1); - String provider = "GOOGLE"; + user.setEmail("test@example.com"); + user.setName("Test User"); + user.setRole(Role.REGISTERED_USER); + + var response = user.generateUserResponse(); + assertEquals("REGISTERED_USER", response.getRole()); + } + @Test + void testRoleEnumValues() { + User userRegistered = new User(); + User userPremium = new User(); + User userModerator = new User(); + + // Test REGISTERED_USER role + userRegistered.setRole(Role.REGISTERED_USER); + assertEquals(Role.REGISTERED_USER, userRegistered.getRole()); + assertEquals("REGISTERED_USER", userRegistered.getRole().name()); + + // Test PREMIUM_USER role + userPremium.setRole(Role.PREMIUM_USER); + assertEquals(Role.PREMIUM_USER, userPremium.getRole()); + assertEquals("PREMIUM_USER", userPremium.getRole().name()); + + // Test MODERATOR role + userModerator.setRole(Role.MODERATOR); + assertEquals(Role.MODERATOR, userModerator.getRole()); + assertEquals("MODERATOR", userModerator.getRole().name()); + } + + @Test + void testUserResponseWithDifferentRoles() { + // Test UserResponse generation with each role + User registeredUser = new User(); + registeredUser.setRole(Role.REGISTERED_USER); + assertEquals("REGISTERED_USER", registeredUser.generateUserResponse().getRole()); + + User premiumUser = new User(); + premiumUser.setRole(Role.PREMIUM_USER); + assertEquals("PREMIUM_USER", premiumUser.generateUserResponse().getRole()); + + User moderatorUser = new User(); + moderatorUser.setRole(Role.MODERATOR); + assertEquals("MODERATOR", moderatorUser.generateUserResponse().getRole()); + } + + @Test + void testUserWithNullRole() { + // Test case for null role + User user = new User(); + assertNull(user.getRole()); + assertNull(user.generateUserResponse().getRole()); + } + + @Test + void testCompleteUserResponseGeneration() { + // Test UserResponse generation with all fields set + User user = new User(); + UUID id = UUID.randomUUID(); user.setId(id); - user.setEmail(email); - user.setPassword(password); - user.setName(name); - user.setVerified(verified); - user.setRole(role); - user.setBirthdate(birthdate); - user.setProvider(provider); + user.setEmail("test@example.com"); + user.setName("Test User"); + user.setRole(Role.PREMIUM_USER); + user.setVerified(true); + user.setBirthdate(LocalDate.of(1990, 1, 1)); + user.setProvider("GOOGLE"); + + var response = user.generateUserResponse(); + assertEquals(id, response.getId()); + assertEquals("test@example.com", response.getEmail()); + assertEquals("Test User", response.getName()); + assertEquals("PREMIUM_USER", response.getRole()); + assertTrue(response.isVerified()); + assertEquals(LocalDate.of(1990, 1, 1), response.getBirthdate()); + assertEquals("GOOGLE", response.getProvider()); + } + + @Test + void testUserResponseWithNullFields() { + // Test UserResponse generation with some null fields + User user = new User(); + user.setEmail("test@example.com"); + user.setName("Test User"); + // Role, birthdate, and provider are null + + var response = user.generateUserResponse(); + assertEquals("test@example.com", response.getEmail()); + assertEquals("Test User", response.getName()); + assertNull(response.getRole()); + assertFalse(response.isVerified()); + assertNull(response.getBirthdate()); + assertNull(response.getProvider()); + } + + @Test + void testVerificationMethodsSetter() { + User user = new User(); + assertFalse(user.isVerified()); + + user.setVerified(true); + assertTrue(user.isVerified()); - // Execute - UserResponse response = user.generateUserResponse(); - - // Verify - assertEquals(id, response.getId(), "ID should match"); - assertEquals(email, response.getEmail(), "Email should match"); - assertEquals(name, response.getName(), "Name should match"); - assertEquals(verified, response.isVerified(), "Verification status should match"); - assertEquals(role, response.getRole(), "Role should match"); - assertEquals(birthdate, response.getBirthdate(), "Birthdate should match"); - assertEquals(provider, response.getProvider(), "Provider should match"); + user.setVerified(false); + assertFalse(user.isVerified()); } } diff --git a/src/test/java/com/safetypin/authentication/repository/UserRepositoryTest.java b/src/test/java/com/safetypin/authentication/repository/UserRepositoryTest.java index fd7e4ff0de472cd55392cdb8ba9b5430095be556..f32c7e276907a4bdf9dce7a8607719c879860333 100644 --- a/src/test/java/com/safetypin/authentication/repository/UserRepositoryTest.java +++ b/src/test/java/com/safetypin/authentication/repository/UserRepositoryTest.java @@ -1,11 +1,14 @@ package com.safetypin.authentication.repository; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; @DataJpaTest @@ -18,22 +21,32 @@ class UserRepositoryTest { void setUp() { userRepository.deleteAll(); - // Create and save a User entity - User user = new User(); - user.setEmail("test@example.com"); - user.setPassword("password"); - user.setName("Test User"); - user.setRole("USER"); - userRepository.save(user); + // Create and save users with different roles + User registeredUser = new User(); + registeredUser.setEmail("registered@example.com"); + registeredUser.setPassword("password"); + registeredUser.setName("Registered User"); + registeredUser.setRole(Role.REGISTERED_USER); + userRepository.save(registeredUser); + + User premiumUser = new User(); + premiumUser.setEmail("premium@example.com"); + premiumUser.setPassword("password"); + premiumUser.setName("Premium User"); + premiumUser.setRole(Role.PREMIUM_USER); + userRepository.save(premiumUser); + + // No moderator user yet - we'll test for missing role } @Test void testFindByEmailWhenUserExists() { // Retrieve the user by email - User foundUser = userRepository.findByEmail("test@example.com"); + User foundUser = userRepository.findByEmail("registered@example.com"); assertNotNull(foundUser, "Expected to find a user with the given email"); - assertEquals("test@example.com", foundUser.getEmail()); - assertEquals("Test User", foundUser.getName()); + assertEquals("registered@example.com", foundUser.getEmail()); + assertEquals("Registered User", foundUser.getName()); + assertEquals(Role.REGISTERED_USER, foundUser.getRole()); } @Test @@ -43,4 +56,29 @@ class UserRepositoryTest { assertNull(foundUser, "Expected no user to be found for a non-existent email"); } + @Test + void testFindByRolesExisting() { + // Test finding users by different existing roles + User registeredUser = userRepository.findByRole(Role.REGISTERED_USER); + assertNotNull(registeredUser); + assertEquals("registered@example.com", registeredUser.getEmail()); + + User premiumUser = userRepository.findByRole(Role.PREMIUM_USER); + assertNotNull(premiumUser); + assertEquals("premium@example.com", premiumUser.getEmail()); + } + + @Test + void testFindByRoleNonExistent() { + // Test finding by role that no user has + User moderator = userRepository.findByRole(Role.MODERATOR); + assertNull(moderator, "No user should be found with MODERATOR role"); + } + + @Test + void testFindAll() { + // Test finding all users + List<User> users = userRepository.findAll(); + assertEquals(2, users.size()); + } } diff --git a/src/test/java/com/safetypin/authentication/seeder/DevDataSeederTest.java b/src/test/java/com/safetypin/authentication/seeder/DevDataSeederTest.java index 72f7cb32c157897980f0e3133b74c0a08be191b6..33caa25146cd47b6cf90e9390774cae75f7c6c83 100644 --- a/src/test/java/com/safetypin/authentication/seeder/DevDataSeederTest.java +++ b/src/test/java/com/safetypin/authentication/seeder/DevDataSeederTest.java @@ -1,5 +1,6 @@ package com.safetypin.authentication.seeder; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import com.safetypin.authentication.repository.UserRepository; import jakarta.transaction.Transactional; @@ -47,7 +48,7 @@ class DevDataSeederTest { user.setPassword(passwordEncoder.encode("test")); user.setName("Existing User"); user.setVerified(true); - user.setRole("admin"); + user.setRole(Role.MODERATOR); user.setBirthdate(LocalDate.of(1990, 1, 1)); user.setProvider("EMAIL"); userRepository.save(user); diff --git a/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java b/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java index 24bf8cb601296ca52e54459644a34434608ba01d..67358617ca0adcb807c47c55c196c98b43e77452 100644 --- a/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java +++ b/src/test/java/com/safetypin/authentication/service/AuthenticationServiceTest.java @@ -3,6 +3,7 @@ package com.safetypin.authentication.service; import com.safetypin.authentication.dto.RegistrationRequest; import com.safetypin.authentication.exception.InvalidCredentialsException; import com.safetypin.authentication.exception.UserAlreadyExistsException; +import com.safetypin.authentication.model.Role; import com.safetypin.authentication.model.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -91,7 +92,7 @@ class AuthenticationServiceTest { savedUser.setPassword("encodedPassword"); savedUser.setName("Test User"); savedUser.setVerified(false); - savedUser.setRole("USER"); + savedUser.setRole(Role.REGISTERED_USER); savedUser.setBirthdate(request.getBirthdate()); savedUser.setProvider("EMAIL"); @@ -129,7 +130,7 @@ class AuthenticationServiceTest { user.setPassword("encodedPassword"); user.setName("Test User"); user.setVerified(true); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(20)); user.setProvider("EMAIL"); @@ -150,7 +151,7 @@ class AuthenticationServiceTest { user.setPassword("encodedPassword"); user.setName("Test User"); user.setVerified(true); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(20)); user.setProvider("EMAIL"); @@ -179,7 +180,7 @@ class AuthenticationServiceTest { user.setPassword("encodedPassword"); user.setName("Test User"); user.setVerified(false); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(20)); user.setProvider("EMAIL"); @@ -224,7 +225,7 @@ class AuthenticationServiceTest { user.setPassword("encodedPassword"); user.setName("Test User"); user.setVerified(true); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(20)); user.setProvider("EMAIL"); @@ -251,7 +252,7 @@ class AuthenticationServiceTest { user.setPassword(null); user.setName("Social User"); user.setVerified(true); - user.setRole("USER"); + user.setRole(Role.REGISTERED_USER); user.setBirthdate(LocalDate.now().minusYears(25)); user.setProvider("GOOGLE"); diff --git a/src/test/java/com/safetypin/authentication/service/EmailServiceTest.java b/src/test/java/com/safetypin/authentication/service/EmailServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a35de63b1a57d3f944cb33e31bc869036d4465a9 --- /dev/null +++ b/src/test/java/com/safetypin/authentication/service/EmailServiceTest.java @@ -0,0 +1,67 @@ +package com.safetypin.authentication.service; + +import com.icegreen.greenmail.configuration.GreenMailConfiguration; +import com.icegreen.greenmail.junit5.GreenMailExtension; +import com.icegreen.greenmail.store.FolderException; +import com.icegreen.greenmail.util.ServerSetup; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; + + +// Email integration test +@SpringBootTest +@ActiveProfiles("test") +@RunWith(SpringRunner.class) +class EmailServiceTest { + @RegisterExtension + static GreenMailExtension greenMail = new GreenMailExtension( + new ServerSetup(2525, "127.0.0.1", "smtp")) + .withConfiguration(GreenMailConfiguration.aConfig().withUser("username", "secret123")) + .withPerMethodLifecycle(false); + + @Autowired + private EmailService emailService; + + @AfterEach + void cleanup() throws FolderException { + greenMail.purgeEmailFromAllMailboxes(); + } + + @Test + void testSendOTPMail_Success() throws MessagingException, IOException, ExecutionException, InterruptedException { + String otp = "123456"; + CompletableFuture<Boolean> future = emailService.sendOTPMail("username@test.com", otp); + boolean status = future.get(); + assertTrue(status); + + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + assertEquals(1, receivedMessages.length); + MimeMessage receivedMessage = receivedMessages[0]; + assertTrue(receivedMessage.getSubject().contains("OTP")); + assertTrue(receivedMessage.getContent().toString().contains(otp)); + } + + @Test + void testSendOTPMail_MailServerDown() throws ExecutionException, InterruptedException { + greenMail.stop(); + + String otp = "123456"; + CompletableFuture<Boolean> future = emailService.sendOTPMail("username@test.com", otp); + boolean status = future.get(); + assertFalse(status); + } +} diff --git a/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java b/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java index cfca094d53014f45858935dfa0f1c97eb354ac48..f2e9b97facb5feb5ad0bc3860d88b81e76beb1f4 100644 --- a/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java +++ b/src/test/java/com/safetypin/authentication/service/OTPServiceTest.java @@ -1,48 +1,127 @@ package com.safetypin.authentication.service; +import com.safetypin.authentication.exception.OTPException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import java.lang.reflect.Constructor; import java.time.LocalDateTime; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) class OTPServiceTest { + @InjectMocks + private OTPService otpService; + + @Mock + private EmailService emailService; + + /** + * Helper method to generate an OTP different from the input + */ + private String generateDifferentOTP(String originalOTP) { + if (originalOTP.equals("000000")) { + return "000001"; + } else { + return "000000"; + } + } @Test void testGenerateOTP() { - OTPService otpService = new OTPService(); + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + String email = "user@example.com"; - String otp = otpService.generateOTP(email); - assertNotNull(otp, "OTP should not be null"); - assertEquals(6, otp.length(), "OTP should be 6 characters long"); - assertTrue(otp.matches("\\d{6}"), "OTP should consist of 6 digits"); + String generatedOTP = otpService.generateOTP(email); + + assertNotNull(generatedOTP, "Generated OTP should not be null"); + assertEquals(6, generatedOTP.length(), "OTP should be 6 digits long"); } @Test - void testVerifyOTPSuccess() { - OTPService otpService = new OTPService(); + void testVerifyOTPWrongOtp() { + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + String email = "user@example.com"; - String otp = otpService.generateOTP(email); - // Immediately verify the generated OTP; it should succeed. - boolean result = otpService.verifyOTP(email, otp); - assertTrue(result, "The OTP should verify successfully"); + String generatedOTP = otpService.generateOTP(email); + + // Generate a different OTP guaranteed to be different from the one generated + String wrongOTP = generateDifferentOTP(generatedOTP); + + boolean result = otpService.verifyOTP(email, wrongOTP); + assertFalse(result, "Verification should fail for an incorrect OTP"); } @Test - void testVerifyOTPWrongOtp() { - OTPService otpService = new OTPService(); + void testMultipleOTPGenerations() { + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + String email = "user@example.com"; - otpService.generateOTP(email); - // Try verifying with an incorrect OTP. - boolean result = otpService.verifyOTP(email, "000000"); - assertFalse(result, "Verification should fail for an incorrect OTP"); + String firstOTP = otpService.generateOTP(email); + String secondOTP = otpService.generateOTP(email); + + assertNotEquals(firstOTP, secondOTP, "Generated OTPs should be different"); + } + + @Test + void testVerifyOTPAfterSecondGeneration() { + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + + String email = "user@example.com"; + String firstOTP = otpService.generateOTP(email); + String secondOTP = otpService.generateOTP(email); + + boolean result = otpService.verifyOTP(email, firstOTP); + assertFalse(result, "First OTP should not verify after second generation"); + + boolean secondResult = otpService.verifyOTP(email, secondOTP); + assertTrue(secondResult, "Latest OTP should verify successfully"); + } + + @Test + void testVerifyOTPMultipleTimes() { + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + + String email = "user@example.com"; + String otp = otpService.generateOTP(email); + + boolean firstTry = otpService.verifyOTP(email, otp); + assertTrue(firstTry, "First verification should succeed"); + + boolean secondTry = otpService.verifyOTP(email, otp); + assertFalse(secondTry, "Second verification should fail as OTP should be consumed"); + } + + @Test + void testNullParameters() { + assertThrows(NullPointerException.class, () -> { + otpService.verifyOTP(null, "123456"); + }, "Should throw exception when email is null"); + + assertThrows(NullPointerException.class, () -> { + otpService.verifyOTP("user@example.com", null); + }, "Should throw exception when OTP is null"); } @Test - void testVerifyOTPExpired() throws Exception { - OTPService otpService = new OTPService(); + void testOTPExpiration() throws Exception { + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + String email = "user@example.com"; String otp = otpService.generateOTP(email); @@ -72,9 +151,64 @@ class OTPServiceTest { @Test void testVerifyOTPWhenNotGenerated() { - OTPService otpService = new OTPService(); // No OTP was generated for this email, so verification should return false. boolean result = otpService.verifyOTP("nonexistent@example.com", "123456"); assertFalse(result, "Verification should fail when no OTP is generated for the given email"); } -} + + @Test + void testGenerateOTPEmailServiceReturnsFalse() { + // Mock email service to return false + when(emailService.sendOTPMail(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(false)); + + String email = "user@example.com"; + + // Verify that OTPException is thrown + OTPException exception = assertThrows(OTPException.class, () -> { + otpService.generateOTP(email); + }, "Should throw OTPException when email service returns false"); + + assertEquals("Failed to send OTP", exception.getMessage()); + } + + @Test + void testGenerateOTPInterruptedException() { + // Mock email service to throw InterruptedException + CompletableFuture<Boolean> future = new CompletableFuture<>(); + future.completeExceptionally(new InterruptedException("Test interrupted")); + when(emailService.sendOTPMail(anyString(), anyString())).thenReturn(future); + + String email = "user@example.com"; + + // Verify that OTPException is thrown + OTPException exception = assertThrows(OTPException.class, () -> { + otpService.generateOTP(email); + }, "Should throw OTPException when InterruptedException occurs"); + + assertTrue(exception.getMessage().contains("Failed to send OTP")); + + // Verify that thread was interrupted + assertTrue(Thread.currentThread().isInterrupted(), "Thread should be interrupted"); + + // Clear the interrupted status for other tests + Thread.interrupted(); + } + + @Test + void testGenerateOTPExecutionException() { + // Mock email service to throw ExecutionException + CompletableFuture<Boolean> future = new CompletableFuture<>(); + future.completeExceptionally(new ExecutionException("Test execution failed", new RuntimeException("Email service error"))); + when(emailService.sendOTPMail(anyString(), anyString())).thenReturn(future); + + String email = "user@example.com"; + + // Verify that OTPException is thrown + OTPException exception = assertThrows(OTPException.class, () -> { + otpService.generateOTP(email); + }, "Should throw OTPException when ExecutionException occurs"); + + assertTrue(exception.getMessage().contains("Failed to send OTP")); + } +} \ No newline at end of file diff --git a/src/test/java/com/safetypin/authentication/service/TokenExpirationTest.java b/src/test/java/com/safetypin/authentication/service/TokenExpirationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..099681cb485a60e35e7405a7b08920f5d008b5b3 --- /dev/null +++ b/src/test/java/com/safetypin/authentication/service/TokenExpirationTest.java @@ -0,0 +1,108 @@ +package com.safetypin.authentication.service; + +import com.safetypin.authentication.dto.UserResponse; +import com.safetypin.authentication.exception.InvalidCredentialsException; +import com.safetypin.authentication.model.User; +import com.safetypin.authentication.repository.UserRepository; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class TokenExpirationTest { + + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private UserService userService; + + @Mock + private OTPService otpService; + + @Mock + private JwtService jwtService; + + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testExpiredTokenThrowsCorrectException() { + // Create a special test-only version of AuthenticationService + TestAuthenticationService testService = new TestAuthenticationService( + userService, passwordEncoder, otpService, jwtService); + + // Create a UUID for our test + UUID userId = UUID.randomUUID(); + User mockUser = new User(); + mockUser.setId(userId); + + // Configure repository to return our user + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + + // Call our test method that forces the isExpired check to be true + InvalidCredentialsException exception = assertThrows( + InvalidCredentialsException.class, + () -> testService.testTokenExpiration(userId) + ); + + // Verify we get the exact "Token expired" exception message + assertEquals("Token expired", exception.getMessage(), + "The exception message should be 'Token expired' when a token is expired"); + } + + // This class extends AuthenticationService to allow us to test specific code paths + private class TestAuthenticationService extends AuthenticationService { + public TestAuthenticationService(UserService userService, + PasswordEncoder passwordEncoder, + OTPService otpService, + JwtService jwtService) { + super(userService, passwordEncoder, otpService, jwtService); + } + + // This method simulates the token expiration check portion of getUserFromJwtToken + public UserResponse testTokenExpiration(UUID userId) { + try { + // Mock a Claims object with an expired date + Claims claims = Jwts.claims() + .setSubject(userId.toString()) + .setIssuedAt(new Date(System.currentTimeMillis() - 200000)) + .setExpiration(new Date(System.currentTimeMillis() - 100000)); // Expired! + + // This is the exact code from the main method that checks expiration + boolean isExpired = claims.getExpiration().before(new Date(System.currentTimeMillis())); + + if (isExpired) { + throw new InvalidCredentialsException("Token expired"); + } + + // Get user from repository (this won't execute in our test) + Optional<User> user = userRepository.findById(userId); + if (user.isEmpty()) { + throw new InvalidCredentialsException("User not found"); + } + return user.get().generateUserResponse(); + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidCredentialsException("Invalid token"); + } + } + } +} diff --git a/src/test/resources/application-dev.properties b/src/test/resources/application-dev.properties index e42e68fd7cebc1b0e033fbdaadfad2e93ffa1545..2ca37e4a61400d18e5a4003082895632093751bb 100644 --- a/src/test/resources/application-dev.properties +++ b/src/test/resources/application-dev.properties @@ -1,14 +1,11 @@ # src/test/resources/application-dev.properties - # Configure H2 in-memory database spring.datasource.url=jdbc:h2:mem:devdb;DB_CLOSE_DELAY=-1 spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= - # JPA/Hibernate configurations spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop - # (Optional) Enable SQL logging -spring.jpa.show-sql=true +spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000000000000000000000000000000000000..18850ac55369aafd59d4bbaad0c9b35d2bd2d35f --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,7 @@ +spring.mail.host=127.0.0.1 +spring.mail.port=2525 +spring.mail.username=username +spring.mail.password=secret123 +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=false +spring.mail.properties.mail.smtp.starttls.required=false