diff --git a/api/swagger.yaml b/api/swagger.yaml index 17ef452d..8415b098 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -652,13 +652,13 @@ components: gender: name: gender in: query - description: preferred gender of the dancers + description: required preferred gender of the dancers schema: type: string range: name: range in: query - description: Range [km] in which the search for dancers should be done + description: Range [km] in which the search for dancers should be done, defaults to 20 km schema: type: string schemas: diff --git a/src/main/java/net/dancier/dancer/core/DancerRepository.java b/src/main/java/net/dancier/dancer/core/DancerRepository.java deleted file mode 100644 index c78470d3..00000000 --- a/src/main/java/net/dancier/dancer/core/DancerRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.dancier.dancer.core; - -import net.dancier.dancer.core.model.Dancer; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; -import java.util.UUID; - -public interface DancerRepository extends JpaRepository { - - Optional findByUserId(UUID userid); - - Boolean existsByDancerName(String dancerName); - -} diff --git a/src/main/java/net/dancier/dancer/core/DancerService.java b/src/main/java/net/dancier/dancer/core/DancerService.java deleted file mode 100644 index 27d890ca..00000000 --- a/src/main/java/net/dancier/dancer/core/DancerService.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.dancier.dancer.core; - -import net.dancier.dancer.chat.dto.DancerDto; -import net.dancier.dancer.chat.dto.DancerIdsDto; -import net.dancier.dancer.core.exception.NotFoundException; -import net.dancier.dancer.core.model.Dancer; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.List; -import java.util.UUID; - -@Service -public class DancerService { - - @Autowired - DancerRepository dancerRepository; - - public Dancer loadByUserId(UUID userId) { - return dancerRepository.findByUserId(userId).orElseThrow(() -> new NotFoundException("No Profile found for given user.")); - } - - public List getAllDancer() { - return dancerRepository.findAll(); - } - - public HashMap getDancerMap(DancerIdsDto dancerIdsDto) { - - HashMap dancers = new HashMap<>(); - - dancerRepository.findAllById(dancerIdsDto.getDancerIds()) - .stream() - .map(DancerDto::fromDancer) - .forEach(dancerDto -> dancers.put(dancerDto.getId(), dancerDto)); - - return dancers; - } -} diff --git a/src/main/java/net/dancier/dancer/core/ProfileService.java b/src/main/java/net/dancier/dancer/core/ProfileService.java index 5d4465b6..495123d2 100644 --- a/src/main/java/net/dancier/dancer/core/ProfileService.java +++ b/src/main/java/net/dancier/dancer/core/ProfileService.java @@ -15,6 +15,7 @@ import net.dancier.dancer.core.model.DanceProfile; import net.dancier.dancer.core.model.Dancer; import net.dancier.dancer.core.util.ModelMapper; +import net.dancier.dancer.dancers.DancerRepository; import net.dancier.dancer.location.ZipCode; import net.dancier.dancer.location.ZipCodeRepository; import org.slf4j.Logger; diff --git a/src/main/java/net/dancier/dancer/chat/DancerController.java b/src/main/java/net/dancier/dancer/dancers/DancerController.java similarity index 67% rename from src/main/java/net/dancier/dancer/chat/DancerController.java rename to src/main/java/net/dancier/dancer/dancers/DancerController.java index 9d673c19..40382aa5 100644 --- a/src/main/java/net/dancier/dancer/chat/DancerController.java +++ b/src/main/java/net/dancier/dancer/dancers/DancerController.java @@ -1,9 +1,10 @@ -package net.dancier.dancer.chat; +package net.dancier.dancer.dancers; import lombok.RequiredArgsConstructor; import net.dancier.dancer.chat.dto.DancerDto; import net.dancier.dancer.chat.dto.DancerIdsDto; -import net.dancier.dancer.core.DancerService; +import net.dancier.dancer.core.dto.PublicProfileDto; +import net.dancier.dancer.core.model.Gender; import net.dancier.dancer.security.AuthenticatedUser; import net.dancier.dancer.security.CurrentUser; import org.slf4j.Logger; @@ -12,8 +13,7 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.UUID; +import java.util.*; import static net.dancier.dancer.authentication.Constants.ROLE_USER; @@ -25,6 +25,17 @@ public class DancerController { private final DancerService dancerService; + @GetMapping("") + @Secured(ROLE_USER) + public ResponseEntity> get( + @CurrentUser AuthenticatedUser authenticatedUser, + @RequestParam Gender gender, + @RequestParam(defaultValue = "20") int range + ) { + log.info("Fetching list of dancers in {} km range with gender {} for user {}", range, gender, authenticatedUser.getUserId()); + return ResponseEntity.ok(dancerService.getDancerList(authenticatedUser, gender, range)); + } + @PostMapping("") @Secured(ROLE_USER) public ResponseEntity> post( diff --git a/src/main/java/net/dancier/dancer/dancers/DancerRepository.java b/src/main/java/net/dancier/dancer/dancers/DancerRepository.java new file mode 100644 index 00000000..abcb9a6a --- /dev/null +++ b/src/main/java/net/dancier/dancer/dancers/DancerRepository.java @@ -0,0 +1,25 @@ +package net.dancier.dancer.dancers; + +import net.dancier.dancer.core.model.Dancer; +import net.dancier.dancer.core.model.Gender; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface DancerRepository extends JpaRepository { + + Optional findByUserId(UUID userid); + + Boolean existsByDancerName(String dancerName); + + List findFirst500ByGenderAndLongitudeBetweenAndLatitudeBetween( + Gender gender, + double lowerLongitude, + double upperLongitude, + double lowerLatitude, + double upperLatitude + ); + +} diff --git a/src/main/java/net/dancier/dancer/dancers/DancerService.java b/src/main/java/net/dancier/dancer/dancers/DancerService.java new file mode 100644 index 00000000..8803e7bf --- /dev/null +++ b/src/main/java/net/dancier/dancer/dancers/DancerService.java @@ -0,0 +1,65 @@ +package net.dancier.dancer.dancers; + +import net.dancier.dancer.chat.dto.DancerDto; +import net.dancier.dancer.chat.dto.DancerIdsDto; +import net.dancier.dancer.core.dto.PublicProfileDto; +import net.dancier.dancer.core.exception.NotFoundException; +import net.dancier.dancer.core.model.Dancer; +import net.dancier.dancer.core.model.Gender; +import net.dancier.dancer.security.AuthenticatedUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +@Service +public class DancerService { + + @Autowired + DancerRepository dancerRepository; + + public Dancer loadByUserId(UUID userId) { + return dancerRepository.findByUserId(userId).orElseThrow(() -> new NotFoundException("No Profile found for given user.")); + } + + public List getAllDancer() { + return dancerRepository.findAll(); + } + + public HashMap getDancerMap(DancerIdsDto dancerIdsDto) { + + HashMap dancers = new HashMap<>(); + + dancerRepository.findAllById(dancerIdsDto.getDancerIds()) + .stream() + .map(DancerDto::fromDancer) + .forEach(dancerDto -> dancers.put(dancerDto.getId(), dancerDto)); + + return dancers; + } + + public List getDancerList(AuthenticatedUser authenticatedUser, Gender gender, int range) { + + Dancer dancer = loadByUserId(authenticatedUser.getUserId()); + + // 1° in longitude in Germany (latitude 47) are 75,78 km + double longitudeRange = range/75.78; + // 1° in longitude are 112,12 km + double latitudeRange = range/112.12; + + double upperLatitude = dancer.getLatitude() + latitudeRange; + double lowerLatitude = dancer.getLatitude() - latitudeRange; + double upperLongitude = dancer.getLongitude() + longitudeRange; + double lowerLongitude = dancer.getLongitude() - longitudeRange; + + List resultList = dancerRepository.findFirst500ByGenderAndLongitudeBetweenAndLatitudeBetween( + gender, lowerLongitude, upperLongitude, lowerLatitude, upperLatitude); + + return resultList.stream() + .map(PublicProfileDto::of) + .filter(d -> d.getId() != dancer.getId()) + .toList(); + } +} diff --git a/src/main/java/net/dancier/dancer/recommendation/RecommendationService.java b/src/main/java/net/dancier/dancer/recommendation/RecommendationService.java index f1256c9d..6a58416a 100644 --- a/src/main/java/net/dancier/dancer/recommendation/RecommendationService.java +++ b/src/main/java/net/dancier/dancer/recommendation/RecommendationService.java @@ -1,7 +1,7 @@ package net.dancier.dancer.recommendation; import lombok.RequiredArgsConstructor; -import net.dancier.dancer.core.DancerRepository; +import net.dancier.dancer.dancers.DancerRepository; import net.dancier.dancer.core.model.Dancer; import net.dancier.dancer.recommendation.model.BaseRecommendation; import net.dancier.dancer.recommendation.model.RecommendationWrapper; diff --git a/src/main/java/net/dancier/dancer/security/CustomUserDetailsServiceImpl.java b/src/main/java/net/dancier/dancer/security/CustomUserDetailsServiceImpl.java index 978455b9..f418aa40 100644 --- a/src/main/java/net/dancier/dancer/security/CustomUserDetailsServiceImpl.java +++ b/src/main/java/net/dancier/dancer/security/CustomUserDetailsServiceImpl.java @@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor; import net.dancier.dancer.authentication.model.User; import net.dancier.dancer.authentication.repository.UserRepository; -import net.dancier.dancer.core.DancerRepository; +import net.dancier.dancer.dancers.DancerRepository; import net.dancier.dancer.core.model.Dancer; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; diff --git a/src/test/java/net/dancier/dancer/chat/ChatControllerTest.java b/src/test/java/net/dancier/dancer/chat/ChatControllerTest.java index 8d49f02d..8d67f940 100644 --- a/src/test/java/net/dancier/dancer/chat/ChatControllerTest.java +++ b/src/test/java/net/dancier/dancer/chat/ChatControllerTest.java @@ -4,21 +4,17 @@ import net.dancier.dancer.AbstractPostgreSQLEnabledTest; import net.dancier.dancer.chat.client.ChatServiceClient; import net.dancier.dancer.chat.dto.*; -import net.dancier.dancer.core.DancerRepository; +import net.dancier.dancer.dancers.DancerRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.ResultActions; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; diff --git a/src/test/java/net/dancier/dancer/dancers/DancerControllerTest.java b/src/test/java/net/dancier/dancer/dancers/DancerControllerTest.java new file mode 100644 index 00000000..b427b376 --- /dev/null +++ b/src/test/java/net/dancier/dancer/dancers/DancerControllerTest.java @@ -0,0 +1,69 @@ +package net.dancier.dancer.dancers; + +import net.dancier.dancer.AbstractPostgreSQLEnabledTest; +import org.junit.jupiter.api.Test; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.jdbc.Sql; +import java.util.List; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isA; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Sql(value = {"/dancers/data.sql"}) +public class DancerControllerTest extends AbstractPostgreSQLEnabledTest { + + @Test + @WithUserDetails("user-with-a-profile@dancier.net") + void getDancersShouldReturnFilteredProfiles() throws Exception { + + mockMvc + .perform(get("/dancers") + .param("range", "20") + .param("gender", "FEMALE") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.*", isA(List.class))) + .andExpect(jsonPath("$.*", hasSize(1))) + .andExpect(jsonPath("$[0].id").value("503ffad4-148b-4af1-8365-62315ff89b9f")) + .andExpect(jsonPath("$[0].gender").value("FEMALE")) + .andExpect(jsonPath("$[0].dancerName").value("perfect_dancer")) + .andExpect(jsonPath("$[0].aboutMe").value("Hi")) + .andExpect(jsonPath("$[0].age").isNotEmpty()) + .andExpect(jsonPath("$[0].size").value("178")) + .andExpect(jsonPath("$[0].city").value("Dortmund")) + .andExpect(jsonPath("$[0].country").value("GER")); + + } + + @Test + @WithUserDetails("user-with-a-profile@dancier.net") + void getDancersUsingDefaultRange() throws Exception { + + mockMvc + .perform(get("/dancers") + .param("gender", "FEMALE") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.*", isA(List.class))) + .andExpect(jsonPath("$.*", hasSize(1))) + .andExpect(jsonPath("$[0].id").value("503ffad4-148b-4af1-8365-62315ff89b9f")) + .andExpect(jsonPath("$[0].gender").value("FEMALE")) + .andExpect(jsonPath("$[0].dancerName").value("perfect_dancer")) + .andExpect(jsonPath("$[0].aboutMe").value("Hi")) + .andExpect(jsonPath("$[0].age").isNotEmpty()) + .andExpect(jsonPath("$[0].size").value("178")) + .andExpect(jsonPath("$[0].city").value("Dortmund")) + .andExpect(jsonPath("$[0].country").value("GER")); + + } + + @Test + @WithUserDetails("user-with-a-profile@dancier.net") + void shouldFailIfGenderIsNotSet() throws Exception { + mockMvc .perform(get("/dancers") + .param("range", "20") + ).andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/net/dancier/dancer/recommendation/EndToEndRecommendationTest.java b/src/test/java/net/dancier/dancer/recommendation/EndToEndRecommendationTest.java index 397cd210..ac75168c 100644 --- a/src/test/java/net/dancier/dancer/recommendation/EndToEndRecommendationTest.java +++ b/src/test/java/net/dancier/dancer/recommendation/EndToEndRecommendationTest.java @@ -1,7 +1,7 @@ package net.dancier.dancer.recommendation; import net.dancier.dancer.AbstractPostgreSQLEnabledTest; -import net.dancier.dancer.core.DancerRepository; +import net.dancier.dancer.dancers.DancerRepository; import net.dancier.dancer.core.model.Dancer; import net.dancier.dancer.recommendation.dto.RecommendationDto; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/resources/dancers/data.sql b/src/test/resources/dancers/data.sql new file mode 100644 index 00000000..f6f99d52 --- /dev/null +++ b/src/test/resources/dancers/data.sql @@ -0,0 +1,116 @@ +-- 5 test users are added +-- user-with-a-profile@dancier.net +-- -> the user that initiates the query +-- user-matching-criterias@dancier.net +-- -> a user that matches all query parameters and is returned by GET /dancers request +-- user-without-matching-gender@dancier.net +-- -> a user that matches all query parameters except gender and is returned by GET /dancers request +-- user-without-matching-latitude@dancier.net +-- -> valid user with a profile but not matching the latitude +-- user-without-matching-longitude@dancier.net +-- -> valid user with a profile but not matching the longitude + +-- user who initiates the query +INSERT + INTO users (id, email, password, email_validated ) + VALUES ( + '55bbf334-6649-11ed-8f65-5b299f0e161f', + 'user-with-a-profile@dancier.net', + '$2a$10$GOChyBEqco9m3wZwkh0RqOTwyWq4HmocguPPfEraSgnbmlrM4.Fey', + true + ); + +INSERT + INTO user_roles (user_id, role_id) +SELECT '55bbf334-6649-11ed-8f65-5b299f0e161f', + id + FROM roles + WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); + +INSERT + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('55bbf334-6649-11ed-8f65-5b299f0e161f', '11065e54-664a-11ed-872e-1b1eb88b44b6', 'good_dancer', '178', '2000-11-11', 'MALE', 'GER', 'Dortmund', '7.1075023', '51.4429498', 'Hi'); + +-- one ordinary user with matching profile +INSERT + INTO users (id, email, password, email_validated ) + VALUES ( + 'b8300af3-a27b-41ed-b35b-3735b25a04df', + 'user-matching-criterias@dancier.net', + '$2a$10$GOChyh6tco9m3wZwkh0RqOTwyWq4HmocguPPfEraSgnbmlrM4.Fax', + true + ); + +INSERT + INTO user_roles (user_id, role_id) +SELECT 'b8300af3-a27b-41ed-b35b-3735b25a04df', + id + FROM roles + WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); + +INSERT + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('b8300af3-a27b-41ed-b35b-3735b25a04df', '503ffad4-148b-4af1-8365-62315ff89b9f', 'perfect_dancer', '178', '1998-11-11', 'FEMALE', 'GER', 'Dortmund', '7.0075023', '51.3429498', 'Hi'); + +-- one ordinary user with not matching gender +INSERT + INTO users (id, email, password, email_validated ) + VALUES ( + 'b90e6478-19d9-492e-97a8-df1f8f7c3901', + 'user-without-matching-gender@dancier.net', + '$2a$10$GOChyh6tco9zu6Zwkh0RqOTwyWq4HmocguPPfEraSgnbmlrM4.Fax', + true + ); + +INSERT + INTO user_roles (user_id, role_id) +SELECT 'b90e6478-19d9-492e-97a8-df1f8f7c3901', + id + FROM roles + WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); + +INSERT + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('b90e6478-19d9-492e-97a8-df1f8f7c3901', '0948c9ba-75a9-4821-8701-0cd3e564f10e', 'Horst', '178', '1980-11-11', 'MALE', 'GER', 'Dortmund', '7.0075023', '51.3429498', 'Hi'); + +-- one ordinary user with not matching latitude +INSERT + INTO users (id, email, password, email_validated ) + VALUES ( + '6cd96820-ce61-4f72-9fa4-6f8900bb7494', + 'user-without-matching-latitude@dancier.net', + '$2a$10$GOChyh6tco9m3wZwkk8tqOTwyWq4HmocguPPfEraSgnbmlrM4.Fax', + true + ); + +INSERT + INTO user_roles (user_id, role_id) +SELECT '6cd96820-ce61-4f72-9fa4-6f8900bb7494', + id + FROM roles + WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); + +INSERT + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('6cd96820-ce61-4f72-9fa4-6f8900bb7494', '9593cd4c-bff4-41b5-b7d8-a632a526721a', 'dancer', '178', '1997-11-11', 'FEMALE', 'GER', 'Bremen', '7.0075023', '54.3429498', 'Hi'); + +-- one ordinary user with not matching longitude +INSERT + INTO users (id, email, password, email_validated ) + VALUES ( + 'e177c7f1-1082-4bd1-ad72-fd88de5b19b2', + 'user-without-matching-longitude@dancier.net', + '$2a$10$GOChyh6tco9m3wZwkk8tqOTwyWq4HmocguPPfEraSgnbmlrM4.Fax', + true + ); + +INSERT + INTO user_roles (user_id, role_id) +SELECT 'e177c7f1-1082-4bd1-ad72-fd88de5b19b2', + id + FROM roles + WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); + +INSERT + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('e177c7f1-1082-4bd1-ad72-fd88de5b19b2', 'f140bc96-5d65-4e6b-8727-f89f2afef03d', 'dancing_queen', '178', '1995-11-11', 'FEMALE', 'GER', 'Essen', '6.0075023', '51.3429498', 'Hi'); diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 25545bde..ffbc59df 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -41,8 +41,8 @@ SELECT '55bbf334-6649-11ed-8f65-5b299f0e161f', WHERE name IN ('ROLE_USER', 'ROLE_HUMAN'); INSERT - INTO dancer(user_id, id) -VALUES ('55bbf334-6649-11ed-8f65-5b299f0e161f', '11065e54-664a-11ed-872e-1b1eb88b44b6'); + INTO dancer(user_id, id, dancer_name, size, birth_date, gender, country, city, longitude, latitude, about_me) +VALUES ('55bbf334-6649-11ed-8f65-5b299f0e161f', '11065e54-664a-11ed-872e-1b1eb88b44b6', 'good_dancer', '178', '2000-11-11', 'MALE', 'GER', 'Dortmund', '7.1075023', '51.4429498', 'Hi'); -- one admin -- no profile attached