From 9bb40249ea7fb0743b59c30a70945477935ad3f0 Mon Sep 17 00:00:00 2001 From: Ksan Date: Wed, 3 Jun 2026 23:34:22 +0200 Subject: [PATCH] forgot to add tests, and removed gradle cache --- .gitea/workflows/ci.yaml | 1 - backend/build.gradle | 13 + .../config/FirebaseConfig.java | 6 + .../etfoglasiserver/config/JwtFilter.java | 1 + .../config/SecurityConfig.java | 7 + .../controller/DeviceTokenController.java | 24 +- .../controller/UserController.java | 25 +- .../etfoglasiserver/dto/UserCreationDTO.java | 5 + .../etfoglasiserver/dto/UserLoginDTO.java | 3 + .../service/DeviceTokenService.java | 29 ++- .../service/NotificationService.java | 50 ++-- .../service/SubjectService.java | 8 +- .../etfoglasiserver/service/UserService.java | 36 +-- .../controller/DeviceTokenControllerTest.java | 44 +++- .../UserControllerIntegrationTest.java | 227 +++++++++++++++++- .../service/NotificationServiceTest.java | 73 +++++- 16 files changed, 468 insertions(+), 84 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index c5ca909..b32c600 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -17,7 +17,6 @@ jobs: with: java-version: '17' distribution: 'temurin' - cache: gradle - name: Make Gradle wrapper executable run: chmod +x ./gradlew diff --git a/backend/build.gradle b/backend/build.gradle index 80d0dfc..67d1e05 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -42,6 +42,15 @@ dependencies { implementation("org.projectlombok:lombok:1.18.42") annotationProcessor 'org.projectlombok:lombok:1.18.42' implementation 'com.google.firebase:firebase-admin:9.9.0' + + + + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation("org.testcontainers:postgresql:1.21.3") + testImplementation("org.testcontainers:junit-jupiter:1.21.4") + testImplementation("org.mockito:mockito-inline:5.2.0") + testImplementation 'org.mockito:mockito-core' } generateJava { @@ -52,4 +61,8 @@ generateJava { tasks.named('test') { useJUnitPlatform() + + jvmArgs += [ + "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('byte-buddy-agent') }}" + ] } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java b/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java index dbe5741..f2efcef 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java @@ -3,6 +3,7 @@ package dev.ksan.etfoglasiserver.config; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -28,4 +29,9 @@ public class FirebaseConfig { } return FirebaseApp.getInstance(); } + + @Bean + public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) { + return FirebaseMessaging.getInstance(firebaseApp); + } } \ No newline at end of file diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/config/JwtFilter.java b/backend/src/main/java/dev/ksan/etfoglasiserver/config/JwtFilter.java index 233333c..76520ce 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/config/JwtFilter.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/config/JwtFilter.java @@ -33,6 +33,7 @@ public class JwtFilter extends OncePerRequestFilter { String token = null; String username = null; + if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); username = jwtService.extractEmail(token); diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java b/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java index eb19531..14f94af 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/config/SecurityConfig.java @@ -13,6 +13,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -61,4 +62,10 @@ public class SecurityConfig { } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + } \ No newline at end of file diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/DeviceTokenController.java b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/DeviceTokenController.java index 7d829c5..8d592f0 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/DeviceTokenController.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/DeviceTokenController.java @@ -1,12 +1,8 @@ package dev.ksan.etfoglasiserver.controller; -import dev.ksan.etfoglasiserver.model.DeviceToken; import dev.ksan.etfoglasiserver.model.RegisterTokenRequest; -import dev.ksan.etfoglasiserver.model.User; -import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo; -import dev.ksan.etfoglasiserver.repository.UserRepo; +import dev.ksan.etfoglasiserver.service.DeviceTokenService; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; @@ -18,24 +14,16 @@ import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor public class DeviceTokenController { - private final DeviceTokenRepo deviceTokenRepo; - private final UserRepo userRepo; + private final DeviceTokenService deviceTokenService; @PostMapping("/register") - public ResponseEntity register( + public ResponseEntity register( @RequestBody RegisterTokenRequest request, @AuthenticationPrincipal UserDetails principal) { - User user = userRepo.findByEmail(principal.getUsername()) - .orElseThrow(() -> new RuntimeException("User not found")); - - DeviceToken deviceToken = deviceTokenRepo - .findByFcmToken(request.getToken()) - .orElse(new DeviceToken()); - - deviceToken.setUser(user); - deviceToken.setFcmToken(request.getToken()); - deviceTokenRepo.save(deviceToken); + deviceTokenService.registerToken( + principal.getUsername(), + request.getToken()); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java index c4cd0b5..0e8c379 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java @@ -23,7 +23,7 @@ public class UserController { @Autowired UserService service; - @GetMapping("users/me") + @GetMapping("/users/me") public ResponseEntity getUserById(Authentication authentication) { String email = authentication.getName(); User user = service.getAccountByEmail(email) @@ -46,7 +46,7 @@ public class UserController { - @PostMapping("users/me/notification-type") + @PostMapping("/users/me/notification-type") public ResponseEntity updateNotificationType( @RequestBody Map request, Authentication authentication) { @@ -82,19 +82,32 @@ public class UserController { return ResponseEntity.ok(updatedUser); } + // not the best, should fix this later @PostMapping("/login") public ResponseEntity login(@RequestBody UserLoginDTO user) { - JwtResponseDTO userVer = service.verify(user); + try { + JwtResponseDTO userVer = service.verify(user); + + if (userVer != null) return new ResponseEntity<>(userVer, HttpStatus.OK); + }catch (Exception e) { + + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } - if (userVer != null) return new ResponseEntity<>(userVer, HttpStatus.OK); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } + // not the best, should fix this later too @PostMapping("/register") public ResponseEntity register(@RequestBody UserCreationDTO user) { - UserDTO newUser = service.register(user); - if (newUser != null) return new ResponseEntity<>(newUser, HttpStatus.CREATED); + try { + UserDTO newUser = service.register(user); + if (newUser != null) return new ResponseEntity<>(newUser, HttpStatus.CREATED); + }catch (Exception e) { + + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserCreationDTO.java b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserCreationDTO.java index d5db0c4..7a2285e 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserCreationDTO.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserCreationDTO.java @@ -12,6 +12,11 @@ public class UserCreationDTO { private Set subjectSet; private String notification; + public UserCreationDTO(String mail, String password) { + this.email = mail; + this.password = password; + } + public String getEmail() { return email; } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserLoginDTO.java b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserLoginDTO.java index a1859dc..feb1f55 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserLoginDTO.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserLoginDTO.java @@ -1,5 +1,8 @@ package dev.ksan.etfoglasiserver.dto; +import lombok.AllArgsConstructor; + +@AllArgsConstructor public class UserLoginDTO { private String email; diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/DeviceTokenService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/DeviceTokenService.java index 568f1f4..478a068 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/DeviceTokenService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/DeviceTokenService.java @@ -1,4 +1,31 @@ package dev.ksan.etfoglasiserver.service; +import dev.ksan.etfoglasiserver.model.DeviceToken; +import dev.ksan.etfoglasiserver.model.User; +import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo; +import dev.ksan.etfoglasiserver.repository.UserRepo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor public class DeviceTokenService { -} + + private final DeviceTokenRepo deviceTokenRepo; + private final UserRepo userRepo; + + public void registerToken(String email, String token) { + + User user = userRepo.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + DeviceToken deviceToken = deviceTokenRepo + .findByFcmToken(token) + .orElse(new DeviceToken()); + + deviceToken.setUser(user); + deviceToken.setFcmToken(token); + + deviceTokenRepo.save(deviceToken); + } +} \ No newline at end of file diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java index b6aae73..d8809cb 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/NotificationService.java @@ -1,9 +1,6 @@ package dev.ksan.etfoglasiserver.service; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.FirebaseMessagingException; -import com.google.firebase.messaging.Message; -import com.google.firebase.messaging.Notification; +import com.google.firebase.messaging.*; import dev.ksan.etfoglasiserver.model.DeviceToken; import dev.ksan.etfoglasiserver.model.User; import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo; @@ -16,45 +13,32 @@ import java.util.List; @RequiredArgsConstructor public class NotificationService { - private final DeviceTokenRepo deviceTokenRepository; + private final DeviceTokenRepo deviceTokenRepo; + private final FirebaseMessaging firebaseMessaging; - - public void sendToUser(User user, String title, String body) { + public void sendToUser(User user, String title, String body) + throws FirebaseMessagingException { List tokens = - deviceTokenRepository.findByUserId(user.getId()); + deviceTokenRepo.findByUserId(user.getId()); for (DeviceToken token : tokens) { + Message message = Message.builder() + .setToken(token.getFcmToken()) + .putData("title", title) + .putData("body", body) + .build(); + try { - Message message = Message.builder() - .setToken(token.getFcmToken()) - .setNotification( - Notification.builder() - .setTitle(title) - .setBody(body) - .build() - ) - .build(); + firebaseMessaging.send(message); + } catch (FirebaseMessagingException ex) { - FirebaseMessaging.getInstance().send(message); - - } catch (FirebaseMessagingException e) { - - String errorCode = e.getMessagingErrorCode() != null - ? e.getMessagingErrorCode().name() - : ""; - - System.out.println("Failed token: " + token.getFcmToken()); - - if (errorCode.equals("UNREGISTERED") - || errorCode.equals("INVALID_ARGUMENT")) { - - deviceTokenRepository.delete(token); - System.out.println("Deleted dead token"); + if (ex.getMessagingErrorCode() + == MessagingErrorCode.UNREGISTERED) { + deviceTokenRepo.delete(token); } } } } - } \ No newline at end of file diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/SubjectService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/SubjectService.java index 183ff1d..96509fb 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/SubjectService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/SubjectService.java @@ -1,5 +1,6 @@ package dev.ksan.etfoglasiserver.service; +import com.google.firebase.messaging.FirebaseMessagingException; import dev.ksan.etfoglasiserver.model.Entry; import dev.ksan.etfoglasiserver.model.NotificationMethod; import dev.ksan.etfoglasiserver.model.Subject; @@ -83,7 +84,12 @@ public class SubjectService { if (user.getNotificationMethod() != NotificationMethod.PUSH_NOTIFICATION) continue; - notificationService.sendToUser(user, entry.getTitle(), entry.getParagraph()); + try{ + + notificationService.sendToUser(user, entry.getTitle(), entry.getParagraph()); + }catch (FirebaseMessagingException e) { + e.printStackTrace(); + } } } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java index 2f054be..6a326df 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java @@ -24,6 +24,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; @Service @Transactional @@ -147,28 +148,27 @@ public class UserService { throw new RuntimeException("Invalid email"); } - public JwtResponseDTO verify(UserLoginDTO user) { + public JwtResponseDTO verify(UserLoginDTO user) { - Authentication authentication = authManager.authenticate( - new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()) - ); + authManager.authenticate( + new UsernamePasswordAuthenticationToken( + user.getEmail(), + user.getPassword() + ) + ); - if (!authentication.isAuthenticated()) { - throw new BadCredentialsException("Invalid username or password"); - } + User foundUser = userRepo.findByEmail(user.getEmail()) + .orElseThrow(() -> new RuntimeException("User not found")); - String token = jwtService.generateToken(user.getEmail()); + String token = jwtService.generateToken(user.getEmail()); - User foundUser = userRepo.findByEmail(user.getEmail()) - .orElseThrow(() -> new RuntimeException("User not found after authentication")); - - return new JwtResponseDTO( - token, - foundUser.getEmail(), - foundUser.getId().toString(), - HttpStatus.OK.toString() - ); - } + return new JwtResponseDTO( + token, + foundUser.getEmail(), + foundUser.getId().toString(), + HttpStatus.OK.toString() + ); + } public Optional getAccountByEmail(String email) { return userRepo.findByEmail(email); diff --git a/backend/src/test/java/dev/ksan/etfoglasiserver/controller/DeviceTokenControllerTest.java b/backend/src/test/java/dev/ksan/etfoglasiserver/controller/DeviceTokenControllerTest.java index f7d1f9c..3b0533f 100644 --- a/backend/src/test/java/dev/ksan/etfoglasiserver/controller/DeviceTokenControllerTest.java +++ b/backend/src/test/java/dev/ksan/etfoglasiserver/controller/DeviceTokenControllerTest.java @@ -1,4 +1,44 @@ package dev.ksan.etfoglasiserver.controller; -public class DeviceTokenControllerTest { -} +import dev.ksan.etfoglasiserver.model.RegisterTokenRequest; +import dev.ksan.etfoglasiserver.service.DeviceTokenService; +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 org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UserDetails; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeviceTokenControllerTest { + + @Mock + DeviceTokenService deviceTokenService; + + @InjectMocks + DeviceTokenController controller; + + @Test + void register_savesNewToken() { + + UserDetails principal = mock(UserDetails.class); + when(principal.getUsername()).thenReturn("test@test.com"); + + RegisterTokenRequest req = new RegisterTokenRequest(); + req.setToken("fcm-123"); + + ResponseEntity res = controller.register(req, principal); + + assertEquals(204, res.getStatusCode().value()); + + verify(deviceTokenService).registerToken( + "test@test.com", + "fcm-123" + ); + } +} \ No newline at end of file diff --git a/backend/src/test/java/dev/ksan/etfoglasiserver/controller/UserControllerIntegrationTest.java b/backend/src/test/java/dev/ksan/etfoglasiserver/controller/UserControllerIntegrationTest.java index 98e5b3e..ed37dd1 100644 --- a/backend/src/test/java/dev/ksan/etfoglasiserver/controller/UserControllerIntegrationTest.java +++ b/backend/src/test/java/dev/ksan/etfoglasiserver/controller/UserControllerIntegrationTest.java @@ -1,4 +1,227 @@ package dev.ksan.etfoglasiserver.controller; -public class UserControllerIntegrationTest { -} +import dev.ksan.etfoglasiserver.dto.JwtResponseDTO; +import dev.ksan.etfoglasiserver.dto.UserCreationDTO; +import dev.ksan.etfoglasiserver.dto.UserDTO; +import dev.ksan.etfoglasiserver.dto.UserLoginDTO; +import dev.ksan.etfoglasiserver.model.NotificationMethod; +import dev.ksan.etfoglasiserver.model.User; +import dev.ksan.etfoglasiserver.repository.UserRepo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Import; +import org.springframework.http.*; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Import(UserController.class) +class UserControllerIntegrationTest { + + @Autowired TestRestTemplate restTemplate; + @Autowired UserRepo userRepo; + @Autowired + PasswordEncoder passwordEncoder; + + private String token; + private User testUser; + + @BeforeEach + void setup() { + userRepo.deleteAll(); + + testUser = new User(); + testUser.setEmail("test@test.com"); + testUser.setPassword(passwordEncoder.encode("password")); + testUser.setNotificationMethod(NotificationMethod.PUSH_NOTIFICATION); + userRepo.save(testUser); + + ResponseEntity res = restTemplate.postForEntity( + "/api/login", + new UserLoginDTO("test@test.com", "password"), + JwtResponseDTO.class + ); + token = res.getBody().getToken(); + } + + + @Test + void login_returnsToken_withValidCredentials() { + ResponseEntity res = restTemplate.postForEntity( + "/api/login", + new UserLoginDTO("test@test.com", "password"), + JwtResponseDTO.class + ); + + assertEquals(200, res.getStatusCode().value()); + assertNotNull(res.getBody().getToken()); + } + + @Test + void login_returns400_withWrongPassword() { + ResponseEntity res = restTemplate.postForEntity( + "/api/login", + new UserLoginDTO("test@test.com", "wrongpassword"), + Object.class + ); + + assertEquals(400, res.getStatusCode().value()); + } + + + @Test + void register_createsUser_andReturns201() { + ResponseEntity res = restTemplate.postForEntity( + "/api/register", + new UserCreationDTO("new@test.com", "password123"), + UserDTO.class + ); + + assertEquals(201, res.getStatusCode().value()); + assertNotNull(res.getBody()); + assertEquals("new@test.com", res.getBody().getEmail()); + } + + @Test + void register_returns400_withInvalidData() { + ResponseEntity res = restTemplate.postForEntity( + "/api/register", + new UserCreationDTO("", ""), + Object.class + ); + + assertEquals(400, res.getStatusCode().value()); + } + + + @Test + void getMe_returnsUser_withValidToken() { + ResponseEntity res = restTemplate.exchange( + "/api/users/me", + HttpMethod.GET, + withAuth(null), + UserDTO.class + ); + + assertEquals(200, res.getStatusCode().value()); + assertEquals("test@test.com", res.getBody().getEmail()); + } + + @Test + void getMe_returns401_withoutToken() { + ResponseEntity res = restTemplate.getForEntity("/api/users/me", Object.class); + assertEquals(401, res.getStatusCode().value()); + } + + + @Test + void updateUser_updatesEmail() { + UserCreationDTO update = new UserCreationDTO("updated@test.com", null); + + ResponseEntity res = restTemplate.exchange( + "/api/users/me", + HttpMethod.PUT, + withAuth(update), + UserDTO.class + ); + + assertEquals(200, res.getStatusCode().value()); + assertEquals("updated@test.com", res.getBody().getEmail()); + } + + @Test + void updateUser_returns401_withoutToken() { + ResponseEntity res = restTemplate.exchange( + "/api/users/me", + HttpMethod.PUT, + new HttpEntity<>(new UserCreationDTO("x@x.com", null)), + Object.class + ); + + assertEquals(401, res.getStatusCode().value()); + } + + + @Test + void updateNotificationType_setsNoNotification() { + ResponseEntity res = restTemplate.exchange( + "/api/users/me/notification-type", + HttpMethod.POST, + withAuth(Map.of("notificationType", "NO_NOTIFICATION")), + Object.class + ); + + assertEquals(200, res.getStatusCode().value()); + + User updated = userRepo.findByEmail("test@test.com").get(); + assertEquals(NotificationMethod.NO_NOTIFICATION, updated.getNotificationMethod()); + } + + @Test + void updateNotificationType_setsPushNotification() { + // set to push first + testUser.setNotificationMethod(NotificationMethod.PUSH_NOTIFICATION); + userRepo.save(testUser); + + restTemplate.exchange( + "/api/users/me/notification-type", + HttpMethod.POST, + withAuth(Map.of("notificationType", "PUSH_NOTIFICATION")), + Object.class + ); + + User updated = userRepo.findByEmail("test@test.com").get(); + assertEquals(NotificationMethod.PUSH_NOTIFICATION, updated.getNotificationMethod()); + } + + @Test + void updateNotificationType_returns401_withoutToken() { + ResponseEntity res = restTemplate.postForEntity( + "/api/users/me/notification-type", + Map.of("notificationType", "NO_NOTIFICATION"), + Object.class + ); + + assertEquals(401, res.getStatusCode().value()); + } + + + @Test + void deleteUser_returns204_andRemovesUser() { + ResponseEntity res = restTemplate.exchange( + "/api/users/me", + HttpMethod.DELETE, + withAuth(null), + Void.class + ); + + assertEquals(204, res.getStatusCode().value()); + assertTrue(userRepo.findByEmail("test@test.com").isEmpty()); + } + + @Test + void deleteUser_returns401_withoutToken() { + ResponseEntity res = restTemplate.exchange( + "/api/users/me", + HttpMethod.DELETE, + HttpEntity.EMPTY, + Object.class + ); + + assertEquals(401, res.getStatusCode().value()); + } + + + private HttpEntity withAuth(T body) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + headers.setContentType(MediaType.APPLICATION_JSON); + return new HttpEntity<>(body, headers); + } +} \ No newline at end of file diff --git a/backend/src/test/java/dev/ksan/etfoglasiserver/service/NotificationServiceTest.java b/backend/src/test/java/dev/ksan/etfoglasiserver/service/NotificationServiceTest.java index 78284fa..39a22a7 100644 --- a/backend/src/test/java/dev/ksan/etfoglasiserver/service/NotificationServiceTest.java +++ b/backend/src/test/java/dev/ksan/etfoglasiserver/service/NotificationServiceTest.java @@ -1,4 +1,73 @@ package dev.ksan.etfoglasiserver.service; -public class NotificationServiceTest { -} +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; +import dev.ksan.etfoglasiserver.model.DeviceToken; +import dev.ksan.etfoglasiserver.model.User; +import dev.ksan.etfoglasiserver.repository.DeviceTokenRepo; +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.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + + @Mock + DeviceTokenRepo deviceTokenRepo; + @Mock + FirebaseMessaging firebaseMessaging; + @InjectMocks + NotificationService notificationService; + + @Test + void sendToUser_sendsMessageForEachToken() throws FirebaseMessagingException { + User user = new User(); + DeviceToken t1 = new DeviceToken(); t1.setFcmToken("token-1"); + DeviceToken t2 = new DeviceToken(); t2.setFcmToken("token-2"); + + when(deviceTokenRepo.findByUserId(user.getId())) + .thenReturn(List.of(t1, t2)); + when(firebaseMessaging.send(any())).thenReturn("msg-id"); + + notificationService.sendToUser(user, "Hello", "World"); + + verify(firebaseMessaging, times(2)).send(any(Message.class)); + } + + @Test + void sendToUser_deletesStaleToken_whenUnregistered() throws FirebaseMessagingException { + User user = new User(); + DeviceToken token = new DeviceToken(); + token.setFcmToken("stale-token"); + + when(deviceTokenRepo.findByUserId(user.getId())) + .thenReturn(List.of(token)); + + FirebaseMessagingException ex = mock(FirebaseMessagingException.class); + when(ex.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED); + when(firebaseMessaging.send(any())).thenThrow(ex); + + notificationService.sendToUser(user, "Hello", "World"); + + verify(deviceTokenRepo).delete(token); + } + + @Test + void sendToUser_doesNothing_whenUserHasNoTokens() throws FirebaseMessagingException { + User user = new User(); + when(deviceTokenRepo.findByUserId(user.getId())).thenReturn(List.of()); + + notificationService.sendToUser(user, "Hello", "World"); + + verify(firebaseMessaging, never()).send(any()); + } +} \ No newline at end of file