forgot to add tests, and removed gradle cache
CI / test (push) Failing after 1m18s

This commit is contained in:
2026-06-03 23:34:22 +02:00
parent 4e5dee2e45
commit 9bb40249ea
16 changed files with 468 additions and 84 deletions
-1
View File
@@ -17,7 +17,6 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Make Gradle wrapper executable
run: chmod +x ./gradlew
+13
View File
@@ -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') }}"
]
}
@@ -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);
}
}
@@ -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);
@@ -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();
}
}
@@ -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<Void> 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();
}
@@ -23,7 +23,7 @@ public class UserController {
@Autowired UserService service;
@GetMapping("users/me")
@GetMapping("/users/me")
public ResponseEntity<UserDTO> 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<String, String> 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<JwtResponseDTO> 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<UserDTO> 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);
}
@@ -12,6 +12,11 @@ public class UserCreationDTO {
private Set<Subject> subjectSet;
private String notification;
public UserCreationDTO(String mail, String password) {
this.email = mail;
this.password = password;
}
public String getEmail() {
return email;
}
@@ -1,5 +1,8 @@
package dev.ksan.etfoglasiserver.dto;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class UserLoginDTO {
private String email;
@@ -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);
}
}
@@ -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<DeviceToken> 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);
}
}
}
}
}
@@ -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();
}
}
}
@@ -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<User> getAccountByEmail(String email) {
return userRepo.findByEmail(email);
@@ -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<Void> res = controller.register(req, principal);
assertEquals(204, res.getStatusCode().value());
verify(deviceTokenService).registerToken(
"test@test.com",
"fcm-123"
);
}
}
@@ -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<JwtResponseDTO> res = restTemplate.postForEntity(
"/api/login",
new UserLoginDTO("test@test.com", "password"),
JwtResponseDTO.class
);
token = res.getBody().getToken();
}
@Test
void login_returnsToken_withValidCredentials() {
ResponseEntity<JwtResponseDTO> 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<UserDTO> 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<UserDTO> 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<UserDTO> 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<Void> 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 <T> HttpEntity<T> withAuth(T body) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(body, headers);
}
}
@@ -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());
}
}