diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/HelloController.java b/services/spring-member/src/main/java/tum/devoops/memberservice/controller/HelloController.java
similarity index 92%
rename from services/spring-member/src/main/java/tum/devoops/memberservice/HelloController.java
rename to services/spring-member/src/main/java/tum/devoops/memberservice/controller/HelloController.java
index c88392b..c0715a3 100644
--- a/services/spring-member/src/main/java/tum/devoops/memberservice/HelloController.java
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/controller/HelloController.java
@@ -1,4 +1,4 @@
-package tum.devoops.memberservice;
+package tum.devoops.memberservice.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/controller/MemberController.java b/services/spring-member/src/main/java/tum/devoops/memberservice/controller/MemberController.java
new file mode 100644
index 0000000..f8a244c
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/controller/MemberController.java
@@ -0,0 +1,154 @@
+package tum.devoops.memberservice.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+import tum.devoops.memberservice.service.MemberService;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RestController
+public class MemberController {
+
+ @Autowired
+ MemberService memberService;
+
+ /**
+ * Retrieves all members.
+ *
+ * This endpoint searches the primary database and returns all members.
+ * Only the MemberSummary is returned.
+ *
+ * @return ResponseEntity containing a List of MemberSummary and HTTP 200
+ */
+ @PreAuthorize("hasAnyRole('member', 'admin')")
+ @GetMapping("/")
+ public ResponseEntity> getAllMembers() {
+ return ResponseEntity.ok(memberService.getAllMembers());
+ }
+
+ /**
+ * Retrieves a member given its ID.
+ *
+ * This endpoint searches the primary database and returns the corresponding member to an ID.
+ *
+ * @param id The unique {@link UUID} of the member.
+ * @return ResponseEntity containing a MemberSummary and HTTP 200. If the member is not found an empty
+ * ResponseEntity with HTTP 404 is returned.
+ */
+ @PreAuthorize("hasAnyRole('member', 'admin')")
+ @GetMapping("/{id}")
+ public ResponseEntity getMemberSummaryById(@PathVariable UUID id) {
+ Optional memberOptional = memberService.getMemberSummaryById(id);
+
+ if (memberOptional.isPresent()) {
+ MemberSummary member = memberOptional.get();
+ return ResponseEntity.ok(member);
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+ /**
+ * Retrieves a member with all details given its ID.
+ *
+ * This endpoint searches the primary database and returns the corresponding member to an ID.
+ *
+ * @param id The unique {@link UUID} of the member.
+ * @return ResponseEntity containing a Member and HTTP 200. If the member is not found an empty
+ * ResponseEntity with HTTP 404 is returned.
+ */
+ @PreAuthorize("hasAnyRole('member', 'admin')")
+ @GetMapping("/{id}/details")
+ public ResponseEntity getMemberById(@PathVariable UUID id) {
+ Optional memberOptional = memberService.getMemberById(id);
+
+ if (memberOptional.isPresent()) {
+ Member member = memberOptional.get();
+ return ResponseEntity.ok(member);
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+ /**
+ * Creates a member in the member-db and the corresponding user in keycloak
+ *
+ * This endpoint creates a member and further creates a corresponding user in keycloak using the email of the member as username.
+ * If the email is null, the username is firstName.lastName
+ *
+ * @param memberCreate the member without id.
+ * @return ResponseEntity containing the created member and HTTP 201. If the email or username exists, returns HTTP 400.
+ */
+ @PreAuthorize("hasRole('admin')")
+ @PostMapping("/")
+ public ResponseEntity createMember(@RequestBody MemberCreate memberCreate, @AuthenticationPrincipal Jwt jwt) {
+ Optional optionalMember = memberService.createMember(memberCreate, jwt.getTokenValue());
+
+ if (optionalMember.isEmpty()) {
+ return ResponseEntity.badRequest().build();
+ }
+
+ Member member = optionalMember.get();
+
+ return ResponseEntity.created(URI.create("/" + member.getId())).body(member);
+ }
+
+ /**
+ * Updates a member in the member db and the keycloak db (if email changes)
+ *
+ * This endpoint updates a member and further updates the corresponding user in keycloak.
+ *
+ * @param newMember the updated member.
+ * @return ResponseEntity containing the updated member and HTTP 200. If the member does not exist, return HTTP 404.
+ */
+ @PreAuthorize("hasRole('admin') or hasRole('member') and #newMember.id.toString() == authentication.name")
+ @PutMapping("/{id}")
+ public ResponseEntity updateMember(@PathVariable UUID id, @RequestBody Member newMember, @AuthenticationPrincipal Jwt jwt) {
+
+ Optional newMemberOptional = memberService.updateMember(newMember, jwt.getTokenValue());
+
+ if (newMemberOptional.isEmpty()) {
+ return ResponseEntity.notFound().build();
+ }
+
+ newMember = newMemberOptional.get();
+ return ResponseEntity.ok(newMember);
+ }
+
+ /**
+ * Deletes a member in the member db and the keycloak db
+ *
+ * This endpoint deletes a member and further deletes the corresponding user in keycloak.
+ *
+ * @param id the id of the member to be deleted.
+ * @return Empty response and HTTP 204. If the member does not exist, return HTTP 404.
+ */
+ @PreAuthorize("hasRole('admin')")
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteMember(@PathVariable UUID id, @AuthenticationPrincipal Jwt jwt) {
+
+ boolean isDeleted = memberService.deleteMember(id, jwt.getTokenValue());
+
+ if (isDeleted) {
+ return ResponseEntity.noContent().build();
+ } else {
+ return ResponseEntity.notFound().build();
+ }
+ }
+}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/converter/MemberConverter.java b/services/spring-member/src/main/java/tum/devoops/memberservice/converter/MemberConverter.java
new file mode 100644
index 0000000..863fc99
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/converter/MemberConverter.java
@@ -0,0 +1,57 @@
+package tum.devoops.memberservice.converter;
+
+import tum.devoops.memberservice.entity.MemberEntity;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+
+import java.time.LocalDate;
+import java.util.UUID;
+
+public class MemberConverter {
+ public static Member convertMemberEntityToMember(MemberEntity memberEntity) {
+ return new Member(
+ memberEntity.getId(),
+ memberEntity.getFirstName(),
+ memberEntity.getLastName(),
+ memberEntity.getEmail(),
+ memberEntity.getBirthday(),
+ memberEntity.getPhoneNumber(),
+ memberEntity.getAddress(),
+ memberEntity.getJoiningDate(),
+ memberEntity.getInformation()
+ );
+ }
+
+ public static MemberEntity convertMemberToMemberEntity(Member member) {
+ return new MemberEntity(
+ member.getId(),
+ member.getFirstName(),
+ member.getLastName(),
+ member.getEmail(),
+ member.getBirthday(),
+ member.getPhoneNumber(),
+ member.getAddress(),
+ member.getJoiningDate(),
+ member.getInformation()
+ );
+ }
+
+ public static MemberEntity convertMemberCreateToMemberEntity(MemberCreate memberCreate, UUID id) {
+ return new MemberEntity(
+ id,
+ memberCreate.getFirstName(),
+ memberCreate.getLastName(),
+ memberCreate.getEmail(),
+ memberCreate.getBirthday(),
+ memberCreate.getPhoneNumber(),
+ memberCreate.getAddress(),
+ LocalDate.now(),
+ memberCreate.getInformation()
+ );
+ }
+
+ public static MemberSummary convertMemberEntityToMemberSummary(MemberEntity memberEntity) {
+ return new MemberSummary(memberEntity.getId(), memberEntity.getFirstName(), memberEntity.getLastName(), memberEntity.getEmail());
+ }
+}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/entity/MemberEntity.java b/services/spring-member/src/main/java/tum/devoops/memberservice/entity/MemberEntity.java
index 54233e6..9b1586f 100644
--- a/services/spring-member/src/main/java/tum/devoops/memberservice/entity/MemberEntity.java
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/entity/MemberEntity.java
@@ -9,13 +9,14 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(schema = "member", name = "members")
-@Getter @Setter @NoArgsConstructor
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class MemberEntity {
@Id
@@ -46,4 +47,18 @@ public class MemberEntity {
@Column(name = "information", nullable = true, columnDefinition = "TEXT")
private String information;
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof MemberEntity)) {
+ return false;
+ }
+ MemberEntity other = (MemberEntity) o;
+
+ return id.equals(other.getId()) && firstName.equals(other.getFirstName())
+ && lastName.equals(other.getLastName()) && email.equals(other.getEmail())
+ && birthday.equals(other.getBirthday()) && phoneNumber.equals(other.getPhoneNumber())
+ && address.equals(other.getAddress()) && joiningDate.equals(other.getJoiningDate())
+ && information.equals(other.getInformation());
+ }
}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/service/KeycloakService.java b/services/spring-member/src/main/java/tum/devoops/memberservice/service/KeycloakService.java
new file mode 100644
index 0000000..6205891
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/service/KeycloakService.java
@@ -0,0 +1,97 @@
+package tum.devoops.memberservice.service;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestClient;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+
+import java.net.URI;
+import java.util.UUID;
+
+@Service
+public class KeycloakService {
+
+ private final RestClient restClient;
+ @Value("${keycloak.realm}")
+ private String realm;
+
+ public KeycloakService(RestClient.Builder restClientBuilder, @Value("${keycloak.base-url}") String baseUrl) {
+ this.restClient = restClientBuilder.baseUrl(baseUrl).build();
+ }
+
+ public UUID createUser(MemberCreate member, String bearerToken) throws Exception {
+ String username = member.getEmail() != null ? member.getEmail() : (member.getFirstName() + member.getLastName()).toLowerCase();
+
+ UserRepresentation body = new UserRepresentation(username, member.getFirstName(), member.getLastName(), member.getEmail(), true);
+
+ ResponseEntity response;
+
+ try {
+ response = restClient.post()
+ .uri("/admin/realms/{realm}/users", realm)
+ .header("Authorization", "Bearer " + bearerToken)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(body)
+ .retrieve()
+ .toBodilessEntity();
+ } catch (HttpClientErrorException.Conflict e) {
+ throw new IllegalAccessException("A keycloak user with this username/email already exists");
+ } catch (HttpClientErrorException.Forbidden e) {
+ throw new SecurityException("Insufficient permissions to create a keycloak user");
+ }
+
+ URI location = response.getHeaders().getLocation();
+
+ if (location == null) {
+ throw new IllegalStateException("Keycloak did not return a location header after user creation");
+ }
+
+ String path = location.getPath();
+ return UUID.fromString(path.substring(path.lastIndexOf("/") + 1));
+ }
+
+ public void updateUser(Member member, String bearerToken) throws HttpClientErrorException{
+
+ UserRepresentation body = new UserRepresentation(member.getEmail(), member.getFirstName(),
+ member.getLastName(), member.getEmail(), true);
+
+ try {
+ restClient.put()
+ .uri("/admin/realms/{realm}/users/{id}", realm, member.getId())
+ .header("Authorization", "Bearer " + bearerToken)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(body)
+ .retrieve()
+ .toBodilessEntity();
+ } catch (HttpClientErrorException.Conflict e) {
+ throw new IllegalArgumentException("A Keycloak user with this email already exists");
+ } catch (HttpClientErrorException.Forbidden e) {
+ throw new SecurityException("Insufficient permissions to update this Keycloak user");
+ }
+ }
+
+ public void deleteUser(UUID keycloakId, String bearerToken) throws HttpClientErrorException, SecurityException{
+ try {
+ restClient.delete()
+ .uri("/admin/realms/{realm}/users/{id}", realm, keycloakId)
+ .header("Authorization", "Bearer " + bearerToken)
+ .retrieve()
+ .toBodilessEntity();
+ } catch (HttpClientErrorException.NotFound e) {
+ throw new IllegalArgumentException("Keycloak user not found: " + keycloakId);
+ } catch (HttpClientErrorException.Forbidden e) {
+ throw new SecurityException("Insufficient permissions to delete a keycloak user");
+ }
+ }
+
+ private record UserRepresentation(
+ String username,
+ String firstName,
+ String lastName, String
+ email, boolean enabled
+ ) {}
+}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/service/MemberService.java b/services/spring-member/src/main/java/tum/devoops/memberservice/service/MemberService.java
new file mode 100644
index 0000000..c2cdeb7
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/service/MemberService.java
@@ -0,0 +1,128 @@
+package tum.devoops.memberservice.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import tum.devoops.memberservice.converter.MemberConverter;
+import tum.devoops.memberservice.entity.MemberEntity;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+import tum.devoops.memberservice.repository.MemberRepository;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+public class MemberService {
+
+ @Autowired
+ KeycloakService keycloakService;
+
+ @Autowired
+ MemberRepository memberRepository;
+
+ public List getAllMembers() {
+ List members = memberRepository.findAll();
+ List memberSummaries = new ArrayList<>();
+
+ for (MemberEntity memberEntity : members) {
+ memberSummaries.add(MemberConverter.convertMemberEntityToMemberSummary(memberEntity));
+ }
+
+ return memberSummaries;
+ }
+
+ public Optional getMemberSummaryById(UUID id) {
+ Optional optionalMemberEntity = memberRepository.findById(id);
+
+ if (optionalMemberEntity.isEmpty()) {
+ return Optional.empty();
+ }
+
+ MemberEntity memberEntity = optionalMemberEntity.get();
+ MemberSummary memberSummary = MemberConverter.convertMemberEntityToMemberSummary(memberEntity);
+
+ return Optional.of(memberSummary);
+ }
+
+ public Optional getMemberById(UUID id) {
+ Optional optionalMemberEntity = memberRepository.findById(id);
+
+ if (optionalMemberEntity.isEmpty()) {
+ return Optional.empty();
+ }
+
+ MemberEntity memberEntity = optionalMemberEntity.get();
+ Member member = MemberConverter.convertMemberEntityToMember(memberEntity);
+
+ return Optional.of(member);
+ }
+
+ public Optional createMember(MemberCreate memberCreate, String bearerToken) {
+ // If a member with this email already exists
+ if (memberRepository.findByEmail(memberCreate.getEmail()).isPresent()) {
+ return Optional.empty();
+ }
+
+ UUID id;
+ try {
+ id = keycloakService.createUser(memberCreate, bearerToken);
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+
+ MemberEntity memberEntity = MemberConverter.convertMemberCreateToMemberEntity(memberCreate, id);
+ memberEntity = memberRepository.save(memberEntity);
+
+ Member member = MemberConverter.convertMemberEntityToMember(memberEntity);
+
+ return Optional.of(member);
+ }
+
+ public Optional updateMember(Member member, String bearerToken) {
+
+ Optional memberEntityWithEmail = memberRepository.findByEmail(member.getEmail());
+
+ if (memberEntityWithEmail.isPresent()) {
+ // If a member other than the passed member has the email
+ if (!memberEntityWithEmail.get().getId().equals(member.getId())) {
+ return Optional.empty();
+ }
+ }
+
+ try {
+ keycloakService.updateUser(member, bearerToken);
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+
+ MemberEntity memberEntity = MemberConverter.convertMemberToMemberEntity(member);
+ MemberEntity updatedMemberEntity = memberRepository.save(memberEntity);
+ Member updatedMember = MemberConverter.convertMemberEntityToMember(updatedMemberEntity);
+
+ return Optional.of(updatedMember);
+ }
+
+ public boolean deleteMember(UUID id, String bearerToken) {
+
+ try {
+ keycloakService.deleteUser(id, bearerToken);
+ } catch (Exception e) {
+ return false;
+ }
+
+ Optional optionalMemberEntity = memberRepository.findById(id);
+
+ if (optionalMemberEntity.isEmpty()) {
+ return false;
+ }
+
+ MemberEntity memberEntity = optionalMemberEntity.get();
+ memberRepository.delete(memberEntity);
+
+ return true;
+ }
+
+}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/MemberServiceApplicationTests.java b/services/spring-member/src/test/java/tum/devoops/memberservice/MemberServiceApplicationTests.java
deleted file mode 100644
index 0fc4502..0000000
--- a/services/spring-member/src/test/java/tum/devoops/memberservice/MemberServiceApplicationTests.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package tum.devoops.memberservice;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.TestPropertySource;
-
-/**
- * Context-load smoke test.
- *
- * DataSource and JPA auto-configurations are excluded so the test can run
- * without a live PostgreSQL instance.
- */
-@SpringBootTest(properties = {
- "spring.autoconfigure.exclude=" +
- "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration," +
- "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration"
-})
-@TestPropertySource(properties = {
- "spring.jpa.hibernate.ddl-auto=none"
-})
-class MemberServiceApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/HelloControllerTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/controller/HelloControllerTest.java
similarity index 98%
rename from services/spring-member/src/test/java/tum/devoops/memberservice/HelloControllerTest.java
rename to services/spring-member/src/test/java/tum/devoops/memberservice/controller/HelloControllerTest.java
index c0efa3e..358f358 100644
--- a/services/spring-member/src/test/java/tum/devoops/memberservice/HelloControllerTest.java
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/controller/HelloControllerTest.java
@@ -1,4 +1,4 @@
-package tum.devoops.memberservice;
+package tum.devoops.memberservice.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/controller/MemberControllerTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/controller/MemberControllerTest.java
new file mode 100644
index 0000000..3868e16
--- /dev/null
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/controller/MemberControllerTest.java
@@ -0,0 +1,629 @@
+package tum.devoops.memberservice.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.test.context.support.WithAnonymousUser;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import tum.devoops.memberservice.config.SecurityConfig;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+import tum.devoops.memberservice.service.MemberService;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(MemberController.class)
+@Import(SecurityConfig.class)
+public class MemberControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockitoBean
+ private MemberService memberService;
+
+ MemberSummary memberSummary;
+ MemberSummary memberSummary1;
+ private Member member;
+ private MemberCreate memberCreate;
+ private String mockToken;
+ private Member newMember;
+
+ @BeforeEach
+ void setUp() {
+ UUID id = UUID.randomUUID();
+
+ memberSummary = new MemberSummary(id, "Alice", "Aberdeen", "alice.aberdeen@example.com");
+ memberSummary1 = new MemberSummary(UUID.randomUUID(), "Bob", "Builder", "bob.the.builder@example.com");
+
+ member = new Member(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ LocalDate.now(),
+ "phoneNumber",
+ "address",
+ LocalDate.now(),
+ "information"
+ );
+
+ memberCreate = new MemberCreate();
+ memberCreate.setFirstName(member.getFirstName());
+ memberCreate.setLastName(member.getLastName());
+ memberCreate.setEmail(member.getEmail());
+ memberCreate.setPhoneNumber(member.getPhoneNumber());
+ memberCreate.setAddress(member.getAddress());
+ memberCreate.setInformation(member.getInformation());
+ memberCreate.setBirthday(member.getBirthday());
+
+ mockToken = "mock-token";
+
+ newMember = new Member(
+ id,
+ "newFirstName",
+ "newLastName",
+ "newemail@email.com",
+ LocalDate.now(),
+ "newPhoneNumber",
+ "newAddress",
+ LocalDate.now(),
+ "newInformation"
+ );
+ }
+
+ // Test cases for createMember() endpoint
+
+ // Verifies that a user with role "member" is allowed to get all members
+ @Test
+ @WithMockUser(roles = "member")
+ void getMembersAllowedForMember() throws Exception {
+ when(memberService.getAllMembers()).thenReturn(List.of());
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user wit role "admin" is allowed to get all members
+ @Test
+ @WithMockUser(roles = "admin")
+ void getMembersAllowedForAdmin() throws Exception {
+ when(memberService.getAllMembers()).thenReturn(List.of());
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with a role other than "admin" and "member" is not allowed to get all members (401 forbidden)
+ @Test
+ @WithMockUser(roles = "guest")
+ void getMembersForbiddenForWrongRole() throws Exception {
+ when(memberService.getAllMembers()).thenReturn(List.of());
+ mockMvc.perform(get("/"))
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user is not allowed to get all members (403 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void getMembersUnauthorizedForAnonymous() throws Exception {
+ when(memberService.getAllMembers()).thenReturn(List.of());
+ mockMvc.perform(get("/"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verifies that the content type of the response is application/json
+ @Test
+ @WithMockUser(roles = "member")
+ void getMembersContentType() throws Exception {
+ List list = List.of();
+ when(memberService.getAllMembers()).thenReturn(list);
+
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON));
+ }
+
+ // Verifies that the endpoint returns an empty list if there are no members
+ @Test
+ @WithMockUser(roles = "member")
+ void getMembersEmptyList() throws Exception {
+ List list = List.of();
+ when(memberService.getAllMembers()).thenReturn(list);
+
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andExpect(content().json("[]"));
+ }
+
+ // Verifies that the endpoint returns the list of members correctly when the list is not empty
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberNonEmptyList() throws Exception {
+ List list = List.of(memberSummary, memberSummary1);
+ when(memberService.getAllMembers()).thenReturn(list);
+
+ mockMvc.perform(get("/"))
+ .andExpect(status().isOk())
+ .andExpect(content().json(objectMapper.writeValueAsString(list)));
+ }
+
+ // Test cases for getMemberById() endpoint
+
+ // Verifies that a user with role "member" is allowed to retrieve a member by ID
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberByIdAllowedForMember() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with role "admin" is allowed to retrieve a member by ID
+ @Test
+ @WithMockUser(roles = "admin")
+ void getMemberByIdAllowedForAdmin() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with a role other than admin and member is not allowed to get a member by ID (403 forbidden)
+ @Test
+ @WithMockUser(roles = "guest")
+ void getMemberByIdForbiddenForWrongRole() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user is not allowed to get a member by ID (401 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void getMemberByIdUnauthorizedForAnonymous() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verifies that the content type of the response is application/json
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberByIdContentType() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON));
+ }
+
+ // Verifies that the entire member is returned correctly
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberByIdReturnsCorrectMember() throws Exception {
+ when(memberService.getMemberById(member.getId())).thenReturn(Optional.of(member));
+
+ mockMvc.perform(get(String.format("/%s/details", member.getId())))
+ .andExpect(status().isOk())
+ .andExpect(content().json(objectMapper.writeValueAsString(member)));
+ }
+
+ // Verifies that a 404 not found is returned, when no member for the given id is found
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberByIdReturnsNotFound() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ when(memberService.getMemberById(randomId)).thenReturn(Optional.empty());
+
+ mockMvc.perform(get(String.format("/%s/details", randomId)))
+ .andExpect(status().isNotFound());
+ }
+
+ // Test cases for getMemberSummaryById() endpoint
+
+ // Verifies that a user with role "member" is allowed to retrieve a member by ID
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberSummaryByIdAllowedForMember() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with role "admin" is allowed to retrieve a member by ID
+ @Test
+ @WithMockUser(roles = "admin")
+ void getMemberSummaryByIdAllowedForAdmin() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with a role other than admin and member is not allowed to get a member by ID (403 forbidden)
+ @Test
+ @WithMockUser(roles = "guest")
+ void getMemberSummaryByIdForbiddenForWrongRole() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user is not allowed to get a member by ID (401 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void getMemberSummaryByIdUnauthorizedForAnonymous() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verifies that the content type of the response is application/json
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberSummaryByIdContentType() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId()), UUID.randomUUID()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON));
+ }
+
+ // Verifies that the entire member is returned correctly
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberSummaryByIdReturnsCorrectMember() throws Exception {
+ when(memberService.getMemberSummaryById(member.getId())).thenReturn(Optional.of(memberSummary));
+
+ mockMvc.perform(get(String.format("/%s", member.getId())))
+ .andExpect(status().isOk())
+ .andExpect(content().json(objectMapper.writeValueAsString(memberSummary)));
+ }
+
+ // Verifies that a 404 not found is returned, when no member for the given id is found
+ @Test
+ @WithMockUser(roles = "member")
+ void getMemberSummaryByIdReturnsNotFound() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ when(memberService.getMemberById(randomId)).thenReturn(Optional.empty());
+
+ mockMvc.perform(get(String.format("/%s", randomId)))
+ .andExpect(status().isNotFound());
+ }
+
+ // Test cases for createMember() endpoint
+
+ // Verifies that a user with role "admin" can create a member
+ @Test
+ void createMemberAllowedForAdmin() throws Exception {
+ when(memberService.createMember(memberCreate, mockToken)).thenReturn(Optional.of(member));
+
+ mockMvc.perform(post("/")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(memberCreate))
+ .with(jwt()
+ .jwt(j -> j.tokenValue(mockToken))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ ))
+ .andExpect(status().isCreated())
+ .andExpect(content().json(objectMapper.writeValueAsString(member)));
+
+ }
+
+ // Verifies that a user with role "member" cannot create a member (403 forbidden)
+ @Test
+ @WithMockUser(roles = "member")
+ void createMemberNotAllowedForAdmin() throws Exception {
+ mockMvc.perform(post("/")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(memberCreate))
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user cannot create a member (401 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void createMemberNotAllowedForAnonymousUser() throws Exception {
+ mockMvc.perform(post("/")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(memberCreate))
+ )
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verifies that 400 (bad request) is returned when cannot create the member
+ @Test
+ void createMemberServiceThrows() throws Exception {
+ when(memberService.createMember(memberCreate, mockToken)).thenReturn(Optional.empty());
+
+ mockMvc.perform(post("/")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(memberCreate))
+ .with(jwt()
+ .jwt(j -> j.tokenValue(mockToken))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ ))
+ .andExpect(status().isBadRequest());
+ }
+
+ // Test for updateMember() endpoint
+
+ // Verifies that a user with role "admin" is allowed to update a member
+ @Test
+ void updateMemberAllowedForAdmin() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.of(newMember));
+
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(newMember.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isOk())
+ .andExpect(content().json(objectMapper.writeValueAsString(newMember)));
+ }
+
+ // Verifies that a user wit role "member" is forbidden to update a member that is not himself (401 forbidden)
+ @Test
+ void updateMemberNotAllowedForUserOtherId() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(randomId.toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that a user with role "member" is allowed to update himself
+ @Test
+ void updateMemberAllowedForUserSameId() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.of(newMember));
+
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isOk());
+ }
+
+ // Verifies that a user with an undefined role other than "admin" and "member" is not allowed
+ // to update a member that is not himself (401 forbidden)
+ @Test
+ void updateMemberNotAllowedForUndefinedRoleOtherId() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(randomId.toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_guest"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that a user with an undefined role other than "admin" and "member" is not allowed
+ // to update a member that is himself (401 forbidden)
+ @Test
+ void updateMemberNotAllowedForUndefinedRoleSameId() throws Exception {
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_guest"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user is not allowed to update a member (403 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void updateMemberUnauthorizedForAnonymous() throws Exception {
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verifies that the content type of the response is application/json
+ @Test
+ void updateMemberContentType() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.of(newMember));
+
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON));
+ }
+
+ // Verifies that the endpoint updates the member properly (200 ok)
+ @Test
+ void updateMemberCorrectUpdateForUserSameId() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.of(newMember));
+
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isOk())
+ .andExpect(content().json(objectMapper.writeValueAsString(newMember)));
+ }
+
+ // Verify that a non-existing member cannot be updated by an admin
+ @Test
+ void updateMemberNotFoundNonExistingId() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.empty());
+
+ mockMvc.perform(put(String.format("/%s", UUID.randomUUID()))
+ .with(jwt()
+ .jwt(j -> j.subject(newMember.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isNotFound());
+ }
+
+ // Verify that an non-existing member cannot updated by a user even if the id is the same as the user
+ @Test
+ void updateMemberNotFoundUserSameId() throws Exception {
+ when(memberService.updateMember(eq(newMember), anyString())).thenReturn(Optional.empty());
+
+ mockMvc.perform(put(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(newMember))
+ )
+ .andExpect(status().isNotFound());
+ }
+
+ // Test for deleteMember() endpoint
+
+ // Verifies that a user with role "admin" is allowed to delete a member (204 no content)
+ @Test
+ void deleteMemberAllowedForAdmin() throws Exception {
+ when(memberService.deleteMember(eq(member.getId()), anyString())).thenReturn(true);
+
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ )
+ )
+ .andExpect(status().isNoContent());
+ }
+
+ // Verifies that a user with role "member" is not allowed to delete a member that is not himself
+ @Test
+ @WithMockUser(roles = "member")
+ void deleteMemberNotAllowedForUserOtherId() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(randomId.toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that a user wit role "member" is forbidden to delete a member even though it is himself (401 forbidden)
+ @Test
+ void deleteMemberNotAllowedForUserSameId() throws Exception {
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_member"))
+ )
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that a user with an undefined role other than "admin" and "member" is not allowed
+ // to delete a member that is not himself (401 forbidden)
+ @Test
+ void deleteMemberNotAllowedForUndefinedRoleOtherId() throws Exception {
+ UUID randomId = UUID.randomUUID();
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(randomId.toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_guest"))
+ )
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that a user with an undefined role other than "admin" and "member" is not allowed
+ // to delete a member that is himself (401 forbidden)
+ @Test
+ void deleteMemberNotAllowedForUndefinedRoleSameId() throws Exception {
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_guest"))
+ )
+ )
+ .andExpect(status().isForbidden());
+ }
+
+ // Verifies that an anonymous user is not allowed to delete a member (403 unauthorized)
+ @Test
+ @WithAnonymousUser
+ void deleteMemberUnauthorizedForAnonymous() throws Exception {
+ mockMvc.perform(delete(String.format("/%s", member.getId())))
+ .andExpect(status().isUnauthorized());
+ }
+
+ // Verify that a non-existing member cannot be deleted by an admin
+ @Test
+ @WithMockUser(roles = "admin")
+ void deleteMemberNotFoundNonExistingId() throws Exception {
+ when(memberService.deleteMember(eq(member.getId()), anyString())).thenReturn(false);
+
+ mockMvc.perform(delete(String.format("/%s", member.getId()))
+ .with(jwt()
+ .jwt(j -> j.subject(member.getId().toString()))
+ .authorities(new SimpleGrantedAuthority("ROLE_admin"))
+ )
+ )
+ .andExpect(status().isNotFound());
+ }
+}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/converter/MemberConverterTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/converter/MemberConverterTest.java
new file mode 100644
index 0000000..c2dc632
--- /dev/null
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/converter/MemberConverterTest.java
@@ -0,0 +1,151 @@
+package tum.devoops.memberservice.converter;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import tum.devoops.memberservice.entity.MemberEntity;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+
+import java.time.LocalDate;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MemberConverterTest {
+
+ UUID id;
+
+ private MemberEntity memberEntity;
+ private Member member;
+ private MemberCreate memberCreate;
+
+ @BeforeEach
+ void setUp() {
+ id = UUID.randomUUID();
+ LocalDate birthday = LocalDate.of(1990, 1, 1);
+ LocalDate joiningDate = LocalDate.of(2020, 6, 15);
+
+ memberEntity = new MemberEntity(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ birthday,
+ "phoneNumber",
+ "address",
+ joiningDate,
+ "information"
+ );
+
+ member = new Member(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ birthday,
+ "phoneNumber",
+ "address",
+ joiningDate,
+ "information"
+ );
+
+ memberCreate = new MemberCreate();
+ memberCreate.setFirstName("firstName");
+ memberCreate.setLastName("lastName");
+ memberCreate.setEmail("email@email.com");
+ memberCreate.setBirthday(birthday);
+ memberCreate.setPhoneNumber("phoneNumber");
+ memberCreate.setAddress("address");
+ memberCreate.setInformation("information");
+ }
+
+ // Verifies that every field of a MemberEntity is mapped onto the resulting Member
+ @Test
+ void convertMemberEntityToMemberMapsAllFields() {
+ Member result = MemberConverter.convertMemberEntityToMember(memberEntity);
+
+ assertEquals(id, result.getId());
+ assertEquals(memberEntity.getFirstName(), result.getFirstName());
+ assertEquals(memberEntity.getLastName(), result.getLastName());
+ assertEquals(memberEntity.getEmail(), result.getEmail());
+ assertEquals(memberEntity.getBirthday(), result.getBirthday());
+ assertEquals(memberEntity.getPhoneNumber(), result.getPhoneNumber());
+ assertEquals(memberEntity.getAddress(), result.getAddress());
+ assertEquals(memberEntity.getJoiningDate(), result.getJoiningDate());
+ assertEquals(memberEntity.getInformation(), result.getInformation());
+ }
+
+ // Verifies that every field of a Member is mapped onto the resulting MemberEntity
+ @Test
+ void convertMemberToMemberEntityMapsAllFields() {
+ MemberEntity result = MemberConverter.convertMemberToMemberEntity(member);
+
+ assertEquals(member.getId(), result.getId());
+ assertEquals(member.getFirstName(), result.getFirstName());
+ assertEquals(member.getLastName(), result.getLastName());
+ assertEquals(member.getEmail(), result.getEmail());
+ assertEquals(member.getBirthday(), result.getBirthday());
+ assertEquals(member.getPhoneNumber(), result.getPhoneNumber());
+ assertEquals(member.getAddress(), result.getAddress());
+ assertEquals(member.getJoiningDate(), result.getJoiningDate());
+ assertEquals(member.getInformation(), result.getInformation());
+ }
+
+ // Verifies that the provided id is used, the MemberCreate fields are copied and joiningDate is set to today
+ @Test
+ void convertMemberCreateToMemberEntityMapsFieldsAndSetsJoiningDate() {
+ LocalDate before = LocalDate.now();
+ MemberEntity result = MemberConverter.convertMemberCreateToMemberEntity(memberCreate, id);
+ LocalDate after = LocalDate.now();
+
+ assertEquals(id, result.getId());
+ assertEquals(memberCreate.getFirstName(), result.getFirstName());
+ assertEquals(memberCreate.getLastName(), result.getLastName());
+ assertEquals(memberCreate.getEmail(), result.getEmail());
+ assertEquals(memberCreate.getBirthday(), result.getBirthday());
+ assertEquals(memberCreate.getPhoneNumber(), result.getPhoneNumber());
+ assertEquals(memberCreate.getAddress(), result.getAddress());
+ assertEquals(memberCreate.getInformation(), result.getInformation());
+
+ // joiningDate is overridden with the current date rather than taken from the input
+ assertTrue(!result.getJoiningDate().isBefore(before) && !result.getJoiningDate().isAfter(after));
+ }
+
+ // Verifies that only id, firstName, lastName and email are mapped onto the summary
+ @Test
+ void convertMemberEntityToMemberSummaryMapsSummaryFields() {
+ MemberSummary result = MemberConverter.convertMemberEntityToMemberSummary(memberEntity);
+
+ assertEquals(id, result.getId());
+ assertEquals(memberEntity.getFirstName(), result.getFirstName());
+ assertEquals(memberEntity.getLastName(), result.getLastName());
+ assertEquals(memberEntity.getEmail(), result.getEmail());
+ }
+
+ // Verifies that null optional fields are preserved through the conversion
+ @Test
+ void convertMemberEntityToMemberPreservesNullOptionalFields() {
+ MemberEntity entity = new MemberEntity(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ null,
+ null,
+ null,
+ null,
+ null
+ );
+
+ Member result = MemberConverter.convertMemberEntityToMember(entity);
+
+ assertNull(result.getBirthday());
+ assertNull(result.getPhoneNumber());
+ assertNull(result.getAddress());
+ assertNull(result.getJoiningDate());
+ assertNull(result.getInformation());
+ }
+}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/service/KeycloakServiceTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/service/KeycloakServiceTest.java
new file mode 100644
index 0000000..5444e7c
--- /dev/null
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/service/KeycloakServiceTest.java
@@ -0,0 +1,169 @@
+package tum.devoops.memberservice.service;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+
+import java.time.LocalDate;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+
+class KeycloakServiceTest {
+
+ private static final String BASE_URL = "http://keycloak.test";
+ private static final String REALM = "test-realm";
+ private static final String TOKEN = "mock-token";
+ private static final String USERS_URI = BASE_URL + "/admin/realms/" + REALM + "/users";
+
+ private MockRestServiceServer server;
+ private KeycloakService keycloakService;
+
+ private UUID id;
+ private MemberCreate memberCreate;
+ private Member member;
+
+ @BeforeEach
+ void setUp() {
+ RestClient.Builder builder = RestClient.builder();
+ server = MockRestServiceServer.bindTo(builder).build();
+
+ keycloakService = new KeycloakService(builder, BASE_URL);
+ ReflectionTestUtils.setField(keycloakService, "realm", REALM);
+
+ id = UUID.randomUUID();
+
+ memberCreate = new MemberCreate();
+ memberCreate.setFirstName("firstName");
+ memberCreate.setLastName("lastName");
+ memberCreate.setEmail("email@email.com");
+
+ member = new Member(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ LocalDate.of(1990, 1, 1),
+ "phoneNumber",
+ "address",
+ LocalDate.of(2020, 6, 15),
+ "information"
+ );
+ }
+
+ // Verifies that a successful creation returns the id parsed from the Location header
+ @Test
+ void createUserReturnsIdFromLocationHeader() throws Exception {
+ server.expect(requestTo(USERS_URI))
+ .andRespond(withStatus(HttpStatus.CREATED)
+ .header(HttpHeaders.LOCATION, USERS_URI + "/" + id));
+
+ UUID result = keycloakService.createUser(memberCreate, TOKEN);
+
+ assertEquals(id, result);
+ }
+
+ // Verifies that creation succeeds when no email is set (username falls back to the member's name)
+ @Test
+ void createUserWithoutEmailReturnsId() throws Exception {
+ memberCreate.setEmail(null);
+
+ server.expect(requestTo(USERS_URI))
+ .andRespond(withStatus(HttpStatus.CREATED)
+ .header(HttpHeaders.LOCATION, USERS_URI + "/" + id));
+
+ UUID result = keycloakService.createUser(memberCreate, TOKEN);
+
+ assertEquals(id, result);
+ }
+
+ // Verifies that a 409 conflict is translated into an IllegalAccessException
+ @Test
+ void createUserThrowsOnConflict() {
+ server.expect(requestTo(USERS_URI))
+ .andRespond(withStatus(HttpStatus.CONFLICT));
+
+ assertThrows(IllegalAccessException.class, () -> keycloakService.createUser(memberCreate, TOKEN));
+ }
+
+ // Verifies that a 403 forbidden is translated into a SecurityException
+ @Test
+ void createUserThrowsOnForbidden() {
+ server.expect(requestTo(USERS_URI))
+ .andRespond(withStatus(HttpStatus.FORBIDDEN));
+
+ assertThrows(SecurityException.class, () -> keycloakService.createUser(memberCreate, TOKEN));
+ }
+
+ // Verifies that a creation without a Location header fails with an IllegalStateException
+ @Test
+ void createUserThrowsWhenNoLocationHeader() {
+ server.expect(requestTo(USERS_URI))
+ .andRespond(withStatus(HttpStatus.CREATED));
+
+ assertThrows(IllegalStateException.class, () -> keycloakService.createUser(memberCreate, TOKEN));
+ }
+
+ // Verifies that a successful update completes without throwing
+ @Test
+ void updateUserSucceeds() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.NO_CONTENT));
+
+ keycloakService.updateUser(member, TOKEN);
+ }
+
+ // Verifies that a 409 conflict is translated into an IllegalArgumentException
+ @Test
+ void updateUserThrowsOnConflict() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.CONFLICT));
+
+ assertThrows(IllegalArgumentException.class, () -> keycloakService.updateUser(member, TOKEN));
+ }
+
+ // Verifies that a 403 forbidden is translated into a SecurityException
+ @Test
+ void updateUserThrowsOnForbidden() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.FORBIDDEN));
+
+ assertThrows(SecurityException.class, () -> keycloakService.updateUser(member, TOKEN));
+ }
+
+ // Verifies that a successful deletion completes without throwing
+ @Test
+ void deleteUserSucceeds() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.NO_CONTENT));
+
+ keycloakService.deleteUser(id, TOKEN);
+ }
+
+ // Verifies that a 404 not found is translated into an IllegalArgumentException
+ @Test
+ void deleteUserThrowsOnNotFound() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.NOT_FOUND));
+
+ assertThrows(IllegalArgumentException.class, () -> keycloakService.deleteUser(id, TOKEN));
+ }
+
+ // Verifies that a 403 forbidden is translated into a SecurityException
+ @Test
+ void deleteUserThrowsOnForbidden() {
+ server.expect(requestTo(USERS_URI + "/" + id))
+ .andRespond(withStatus(HttpStatus.FORBIDDEN));
+
+ assertThrows(SecurityException.class, () -> keycloakService.deleteUser(id, TOKEN));
+ }
+}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/service/MemberServiceTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/service/MemberServiceTest.java
new file mode 100644
index 0000000..2b74490
--- /dev/null
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/service/MemberServiceTest.java
@@ -0,0 +1,297 @@
+package tum.devoops.memberservice.service;
+
+import org.junit.jupiter.api.BeforeEach;
+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 tum.devoops.memberservice.entity.MemberEntity;
+import tum.devoops.memberservice.model.Member;
+import tum.devoops.memberservice.model.MemberCreate;
+import tum.devoops.memberservice.model.MemberSummary;
+import tum.devoops.memberservice.repository.MemberRepository;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MemberServiceTest {
+
+ private static final String TOKEN = "mock-token";
+
+ @Mock
+ private MemberRepository memberRepository;
+
+ @Mock
+ private KeycloakService keycloakService;
+
+ @InjectMocks
+ private MemberService memberService;
+
+ private UUID id;
+ private MemberEntity memberEntity;
+ private Member member;
+ private MemberSummary expectedSummary;
+ private MemberCreate memberCreate;
+
+ @BeforeEach
+ void setUp() {
+ id = UUID.randomUUID();
+
+ memberEntity = new MemberEntity(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ LocalDate.of(1990, 1, 1),
+ "phoneNumber",
+ "address",
+ LocalDate.of(2020, 6, 15),
+ "information"
+ );
+
+ member = new Member(
+ id,
+ "firstName",
+ "lastName",
+ "email@email.com",
+ LocalDate.of(1990, 1, 1),
+ "phoneNumber",
+ "address",
+ LocalDate.of(2020, 6, 15),
+ "information"
+ );
+
+ expectedSummary = new MemberSummary(id, "firstName", "lastName", "email@email.com");
+
+ memberCreate = new MemberCreate();
+ memberCreate.setFirstName("firstName");
+ memberCreate.setLastName("lastName");
+ memberCreate.setEmail("email@email.com");
+ memberCreate.setBirthday(LocalDate.of(1990, 1, 1));
+ memberCreate.setPhoneNumber("phoneNumber");
+ memberCreate.setAddress("address");
+ memberCreate.setInformation("information");
+ }
+
+ // Test cases for getAllMembers()
+
+ // Verifies that an empty list is returned when the repository holds no members
+ @Test
+ void getAllMembersReturnsEmptyListWhenNoMembers() {
+ when(memberRepository.findAll()).thenReturn(List.of());
+
+ List result = memberService.getAllMembers();
+
+ assertTrue(result.isEmpty());
+ }
+
+ // Verifies that each entity is converted into a MemberSummary with its fields mapped
+ @Test
+ void getAllMembersReturnsSummaryPerEntity() {
+ when(memberRepository.findAll()).thenReturn(List.of(memberEntity));
+
+ List result = memberService.getAllMembers();
+
+ assertEquals(1, result.size());
+ assertEquals(expectedSummary, result.getFirst());
+ }
+
+ // Test cases for getMemberSummaryById()
+
+ // Verifies that a populated summary is returned when the member exists
+ @Test
+ void getMemberSummaryByIdReturnsSummaryWhenFound() {
+ when(memberRepository.findById(id)).thenReturn(Optional.of(memberEntity));
+
+ Optional result = memberService.getMemberSummaryById(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(expectedSummary, result.get());
+ }
+
+ // Verifies that an empty optional is returned when the member does not exist
+ @Test
+ void getMemberSummaryByIdReturnsEmptyWhenNotFound() {
+ when(memberRepository.findById(id)).thenReturn(Optional.empty());
+
+ Optional result = memberService.getMemberSummaryById(id);
+
+ assertTrue(result.isEmpty());
+ }
+
+ // Test cases for getMemberById()
+
+ // Verifies that the full member is returned when it exists
+ @Test
+ void getMemberByIdReturnsMemberWhenFound() {
+ when(memberRepository.findById(id)).thenReturn(Optional.of(memberEntity));
+
+ Optional result = memberService.getMemberById(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(member, result.get());
+ }
+
+ // Verifies that an empty optional is returned when the member does not exist
+ @Test
+ void getMemberByIdReturnsEmptyWhenNotFound() {
+ when(memberRepository.findById(id)).thenReturn(Optional.empty());
+
+ Optional result = memberService.getMemberById(id);
+
+ assertTrue(result.isEmpty());
+ }
+
+ // Test cases for createMember()
+
+ // Verifies that creation is rejected when a member with the same email already exists
+ @Test
+ void createMemberReturnsEmptyWhenEmailExists() {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.of(memberEntity));
+
+ Optional result = memberService.createMember(memberCreate, TOKEN);
+
+ assertTrue(result.isEmpty());
+ verifyNoInteractions(keycloakService);
+ verify(memberRepository, never()).save(any());
+ }
+
+ // Verifies that creation is rejected and nothing is persisted when Keycloak fails
+ @Test
+ void createMemberReturnsEmptyWhenKeycloakThrows() throws Exception {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.empty());
+ when(keycloakService.createUser(memberCreate, TOKEN)).thenThrow(new RuntimeException("keycloak down"));
+
+ Optional result = memberService.createMember(memberCreate, TOKEN);
+
+ assertTrue(result.isEmpty());
+ verify(memberRepository, never()).save(any());
+ }
+
+ // Verifies that a member is created and returned when the email is free and Keycloak succeeds
+ @Test
+ void createMemberReturnsMemberOnSuccess() throws Exception {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.empty());
+ when(keycloakService.createUser(memberCreate, TOKEN)).thenReturn(id);
+ when(memberRepository.save(any(MemberEntity.class))).thenReturn(memberEntity);
+
+ Optional result = memberService.createMember(memberCreate, TOKEN);
+
+ assertTrue(result.isPresent());
+ assertEquals(member, result.get());
+ verify(memberRepository).save(any(MemberEntity.class));
+ }
+
+ // Test cases for updateMember()
+
+ // Verifies that an update is rejected when the email belongs to a different member
+ @Test
+ void updateMemberReturnsEmptyWhenEmailTakenByOther() {
+ MemberEntity otherMember = new MemberEntity(
+ UUID.randomUUID(),
+ "other",
+ "other",
+ "email@email.com",
+ LocalDate.of(1990, 1, 1),
+ "phoneNumber",
+ "address",
+ LocalDate.of(2020, 6, 15),
+ "information"
+ );
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.of(otherMember));
+
+ Optional result = memberService.updateMember(member, TOKEN);
+
+ assertTrue(result.isEmpty());
+ verifyNoInteractions(keycloakService);
+ verify(memberRepository, never()).save(any());
+ }
+
+ // Verifies that an update succeeds when the email belongs to the same member
+ @Test
+ void updateMemberReturnsMemberWhenEmailBelongsToSameMember() {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.of(memberEntity));
+ when(memberRepository.save(any(MemberEntity.class))).thenReturn(memberEntity);
+
+ Optional result = memberService.updateMember(member, TOKEN);
+
+ assertTrue(result.isPresent());
+ assertEquals(member, result.get());
+ verify(keycloakService).updateUser(member, TOKEN);
+ verify(memberRepository).save(any(MemberEntity.class));
+ }
+
+ // Verifies that an update succeeds when the email is not used by anyone
+ @Test
+ void updateMemberReturnsMemberWhenEmailUnused() {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.empty());
+ when(memberRepository.save(any(MemberEntity.class))).thenReturn(memberEntity);
+
+ Optional result = memberService.updateMember(member, TOKEN);
+
+ assertTrue(result.isPresent());
+ assertEquals(member, result.get());
+ verify(memberRepository).save(any(MemberEntity.class));
+ }
+
+ // Verifies that an update is rejected and nothing is persisted when Keycloak fails
+ @Test
+ void updateMemberReturnsEmptyWhenKeycloakThrows() {
+ when(memberRepository.findByEmail("email@email.com")).thenReturn(Optional.empty());
+ doThrow(new RuntimeException("keycloak down")).when(keycloakService).updateUser(member, TOKEN);
+
+ Optional result = memberService.updateMember(member, TOKEN);
+
+ assertTrue(result.isEmpty());
+ verify(memberRepository, never()).save(any());
+ }
+
+ // Test cases for deleteMember()
+
+ // Verifies that deletion fails and nothing is removed when Keycloak fails
+ @Test
+ void deleteMemberReturnsFalseWhenKeycloakThrows() {
+ doThrow(new RuntimeException("keycloak down")).when(keycloakService).deleteUser(id, TOKEN);
+
+ boolean result = memberService.deleteMember(id, TOKEN);
+
+ assertFalse(result);
+ verify(memberRepository, never()).delete(any());
+ }
+
+ // Verifies that deletion fails when the member does not exist in the repository
+ @Test
+ void deleteMemberReturnsFalseWhenMemberNotFound() {
+ when(memberRepository.findById(id)).thenReturn(Optional.empty());
+
+ boolean result = memberService.deleteMember(id, TOKEN);
+
+ assertFalse(result);
+ verify(memberRepository, never()).delete(any());
+ }
+
+ // Verifies that deletion succeeds and the entity is removed when it exists
+ @Test
+ void deleteMemberReturnsTrueOnSuccess() {
+ when(memberRepository.findById(id)).thenReturn(Optional.of(memberEntity));
+
+ boolean result = memberService.deleteMember(id, TOKEN);
+
+ assertTrue(result);
+ verify(memberRepository).delete(memberEntity);
+ }
+}
diff --git a/services/spring-member/src/test/java/tum/devoops/memberservice/SecurityConfigTest.java b/services/spring-member/src/test/java/tum/devoops/memberservice/service/SecurityConfigTest.java
similarity index 98%
rename from services/spring-member/src/test/java/tum/devoops/memberservice/SecurityConfigTest.java
rename to services/spring-member/src/test/java/tum/devoops/memberservice/service/SecurityConfigTest.java
index 48c6216..bb67044 100644
--- a/services/spring-member/src/test/java/tum/devoops/memberservice/SecurityConfigTest.java
+++ b/services/spring-member/src/test/java/tum/devoops/memberservice/service/SecurityConfigTest.java
@@ -1,4 +1,4 @@
-package tum.devoops.memberservice;
+package tum.devoops.memberservice.service;
import java.util.List;
import java.util.Map;