diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000000..b66c483169 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,24 @@ +name: TourGuide CI with Maven + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build and testing with Maven + run: mvn -B package --file TourGuide/pom.xml diff --git a/.github/workflows/realease.yml b/.github/workflows/realease.yml new file mode 100644 index 0000000000..206ed948a4 --- /dev/null +++ b/.github/workflows/realease.yml @@ -0,0 +1,32 @@ +name: TourGuide Release artefact with Maven + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Compile Artefact + run: mvn package --file TourGuide/pom.xml + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: TourGuide/target/*.jar + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: "Publication automatique de la version ${{ github.ref_name }}" diff --git a/README.md b/README.md new file mode 100644 index 0000000000..f9b42d84bb --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# TourGuide API - Java Project 8 + +## 1. Project Description + +**TourGuide** is a Java and Spring Boot back-end application that simulates an API for a tour guide service. The application's purpose is to track user locations in real-time, suggest nearby tourist attractions, and manage a rewards system. + +The main goal of this project was to refactor an existing application to **drastically optimize its performance** to handle a very high load (up to 100,000 users) concurrently and efficiently. + +--- + +## 2. Key Features + +* **Real-Time Tracking**: Locates and updates the position of multiple users concurrently. +* **Nearby Attractions**: For a given user, returns the 5 closest tourist attractions, along with the distance and associated reward points. +* **Rewards Calculation**: Automatically calculates and assigns reward points when a user visits an attraction. +* **Trip Deals**: Generates a list of offers from travel partners based on user preferences and accumulated reward points. +* **REST API**: Exposes a set of endpoints to interact with the application's features. + +--- + +## 3. Technologies & Libraries + +This project is built with the following technologies, as defined in the `pom.xml` file: + +* **Language**: Java 8 +* **Main Framework**: Spring Boot (`2.1.0.RELEASE`) +* **Web Server**: Tomcat (embedded with Spring Boot) +* **Build & Dependency Management**: Apache Maven +* **Testing**: JUnit 5, Mockito +* **External Libraries (provided as .jar files)**: + * `gpsUtil`: Simulates calls to a GPS location service. + * `RewardCentral`: Simulates calls to a points calculation service. + * `TripPricer`: Simulates calls to a trip pricing service. + +--- + +## 4. Architecture & Performance Optimizations + +To meet the high-performance requirements, the application's architecture was thoroughly refactored. + +### a. Asynchronous Processing with CompletableFuture + +The main bottleneck was the sequential processing of users. The solution was to implement a multi-threaded architecture. + +* **`ExecutorService`**: A thread pool (`newFixedThreadPool`) is used in `TourGuideService` and `RewardsService` to manage tasks in a controlled manner, avoiding the costly creation of threads on the fly. +* **`CompletableFuture`**: The `trackAllUsersLocation` and `calculateAllUsersRewards` methods launch the location tracking and rewards calculation processes completely asynchronously. This allows for the parallel processing of thousands of users. The performance tests leverage these methods to start all tasks and then wait for their global completion using `CompletableFuture.allOf(...).join()`. + +### b. Strategic Caching + +The second performance issue stemmed from repeated calls to slow external resources. + +* **Attractions Cache**: The list of attractions is fetched **only once** at application startup within the `RewardsService` constructor and is stored in memory. This eliminates thousands of redundant and slow calls to the `gpsUtil` library, making the rewards calculation almost instantaneous, as demonstrated by the `highVolumeGetRewards` test. + +### c. Thread Safety + +With parallel processing, it was crucial to prevent race conditions. + +* **Synchronization**: Methods that modify shared data, such as `user.addUserReward()`, have been marked as `synchronized` to ensure that concurrent access is handled safely, without data corruption. + +--- + +## 5. Installation & Setup + +### Prerequisites + +* Java Development Kit (JDK) 8 or higher +* Apache Maven 3+ + +### Steps + +1. **Clone the repository**: + ```bash + git clone [https://github.com/KentinTL/JavaPathENProject8.git](https://github.com/KentinTL/JavaPathENProject8.git) + cd JavaPathENProject8/TourGuide + ``` + +2. **Install local dependencies**: + The project uses local libraries. Make sure they are correctly referenced in the `pom.xml` via ``. + +3. **Build the project with Maven**: + This command will download dependencies, compile the code, and run unit tests. Performance tests are excluded from the default build thanks to the `maven-surefire-plugin` configuration. + ```bash + mvn clean install + ``` + +4. **Run the application**: + ```bash + java -jar target/tourguide-0.0.1-SNAPSHOT.jar + ``` + +The application will start and be accessible at `http://localhost:8080`. + +--- + +## 6. API Endpoints + +The API is defined in `TourGuideController.java` and exposes the following endpoints: + +| Method | Endpoint | Parameters | Description | +| :--- | :--- | :--- | :--- | +| `GET` | `/` | _None_ | Displays a welcome message. | +| `GET` | `/getLocation` | `userName` (String) | Gets the last known location of the user. | +| `GET` | `/getNearbyAttractions` | `userName` (String) | Returns the 5 nearest attractions for the user. | +| `GET` | `/getRewards` | `userName` (String) | Returns the list of rewards earned by the user. | +| `GET` | `/getAllCurrentLocations`| _None_ | Returns the last known location of all users. | +| `GET` | `/getTripDeals` | `userName` (String) | Gets trip deals for the user. | + +**Example usage with cURL:** +```bash +curl "http://localhost:8080/getNearbyAttractions?userName=internalUser1" diff --git a/TourGuide/pom.xml b/TourGuide/pom.xml index d3aaeeb349..9ecce50780 100644 --- a/TourGuide/pom.xml +++ b/TourGuide/pom.xml @@ -16,6 +16,12 @@ 17 + + + local-libs + file://${project.basedir}/libs + + org.springframework.boot @@ -48,21 +54,27 @@ - gpsUtil + com.local gpsUtil 1.0.0 + system + ${project.basedir}/libs/gpsUtil.jar - tripPricer - tripPricer + com.local + RewardCentral 1.0.0 + system + ${project.basedir}/libs/RewardCentral.jar - rewardCentral - rewardCentral + com.local + TripPricer 1.0.0 + system + ${project.basedir}/libs/TripPricer.jar diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java similarity index 70% rename from TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java rename to TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java index c349efdd67..e1a7e7d6df 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourguideApplication.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideApplication.java @@ -4,10 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class TourguideApplication { +public class TourGuideApplication { public static void main(String[] args) { - SpringApplication.run(TourguideApplication.class, args); + SpringApplication.run(TourGuideApplication.class, args); } } diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java index a884e6590b..298352c27b 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/TourGuideController.java @@ -2,12 +2,13 @@ import java.util.List; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import gpsUtil.location.Attraction; import gpsUtil.location.VisitedLocation; import com.openclassrooms.tourguide.service.TourGuideService; @@ -41,10 +42,10 @@ public VisitedLocation getLocation(@RequestParam String userName) { // The distance in miles between the user's location and each of the attractions. // The reward points for visiting each Attraction. // Note: Attraction reward points can be gathered from RewardsCentral - @RequestMapping("/getNearbyAttractions") - public List getNearbyAttractions(@RequestParam String userName) { - VisitedLocation visitedLocation = tourGuideService.getUserLocation(getUser(userName)); - return tourGuideService.getNearByAttractions(visitedLocation); + @RequestMapping("/getNearbyAttractions") + public List getNearbyAttractions(@RequestParam String userName) { + User user = getUser(userName); + return tourGuideService.getNearByAttractionDtos(user); } @RequestMapping("/getRewards") diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java new file mode 100644 index 0000000000..4e6b45401b --- /dev/null +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/dto/NearByAttractionDto.java @@ -0,0 +1,20 @@ +package com.openclassrooms.tourguide.dto; + +import gpsUtil.location.Location; + +public class NearByAttractionDto { + public String attractionName; + public Location attractionLocation; + public Location userLocation; + public double distance; + public int rewardPoints; + + public NearByAttractionDto(String attractionName, Location attractionLocation, + Location userLocation, double distance, int rewardPoints) { + this.attractionName = attractionName; + this.attractionLocation = attractionLocation; + this.userLocation = userLocation; + this.distance = distance; + this.rewardPoints = rewardPoints; + } +} \ No newline at end of file diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java index ad440eb484..4c3302438a 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/RewardsService.java @@ -1,6 +1,11 @@ package com.openclassrooms.tourguide.service; +import java.util.Comparator; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -16,65 +21,82 @@ public class RewardsService { private static final double STATUTE_MILES_PER_NAUTICAL_MILE = 1.15077945; - // proximity in miles - private int defaultProximityBuffer = 10; - private int proximityBuffer = defaultProximityBuffer; - private int attractionProximityRange = 200; - private final GpsUtil gpsUtil; - private final RewardCentral rewardsCentral; - - public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { - this.gpsUtil = gpsUtil; - this.rewardsCentral = rewardCentral; - } - - public void setProximityBuffer(int proximityBuffer) { - this.proximityBuffer = proximityBuffer; - } - - public void setDefaultProximityBuffer() { - proximityBuffer = defaultProximityBuffer; - } - - public void calculateRewards(User user) { - List userLocations = user.getVisitedLocations(); - List attractions = gpsUtil.getAttractions(); - - for(VisitedLocation visitedLocation : userLocations) { - for(Attraction attraction : attractions) { - if(user.getUserRewards().stream().filter(r -> r.attraction.attractionName.equals(attraction.attractionName)).count() == 0) { - if(nearAttraction(visitedLocation, attraction)) { - user.addUserReward(new UserReward(visitedLocation, attraction, getRewardPoints(attraction, user))); - } - } - } - } - } - - public boolean isWithinAttractionProximity(Attraction attraction, Location location) { - return getDistance(attraction, location) > attractionProximityRange ? false : true; - } - - private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) { - return getDistance(attraction, visitedLocation.location) > proximityBuffer ? false : true; - } - - private int getRewardPoints(Attraction attraction, User user) { - return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); - } - - public double getDistance(Location loc1, Location loc2) { + // proximity in miles + private static final int defaultProximityBuffer = 10; + private int proximityBuffer = defaultProximityBuffer; + private int attractionProximityRange = 200; + private final GpsUtil gpsUtil; + private final RewardCentral rewardsCentral; +// private final List attractions; + private final ExecutorService executorService = Executors.newFixedThreadPool(100); + + public RewardsService(GpsUtil gpsUtil, RewardCentral rewardCentral) { + this.gpsUtil = gpsUtil; + this.rewardsCentral = rewardCentral; +// this.attractions = gpsUtil.getAttractions(); + } + + public void setProximityBuffer(int proximityBuffer) { + this.proximityBuffer = proximityBuffer; + } + + public void setDefaultProximityBuffer() { + proximityBuffer = defaultProximityBuffer; + } + + public void calculateAllUsersRewards(List users) { + List> futures = users.stream() + .map(u -> CompletableFuture.runAsync(() -> calculateRewards(u), executorService)) + .toList(); + + futures.forEach(CompletableFuture::join); + } + + public void calculateRewards(User user) { + List userLocations = user.getVisitedLocations(); + List attractions = gpsUtil.getAttractions(); + for (VisitedLocation visitedLocation : userLocations) { + for (Attraction attraction : attractions) { + if (user.getUserRewards().stream().noneMatch(r -> r.attraction.attractionName.equals(attraction.attractionName))) { + if (nearAttraction(visitedLocation, attraction)) { + user.addUserReward(new UserReward(visitedLocation, attraction)); + } + } + } + } + } + + public boolean isWithinAttractionProximity(Attraction attraction, Location location) { + return !(getDistance(attraction, location) > attractionProximityRange); + } + + private boolean nearAttraction(VisitedLocation visitedLocation, Attraction attraction) { + return !(getDistance(attraction, visitedLocation.location) > proximityBuffer); + } + + public int getRewardPoints(Attraction attraction, User user) { + return rewardsCentral.getAttractionRewardPoints(attraction.attractionId, user.getUserId()); + } + + public double getDistance(Location loc1, Location loc2) { double lat1 = Math.toRadians(loc1.latitude); double lon1 = Math.toRadians(loc1.longitude); double lat2 = Math.toRadians(loc2.latitude); double lon2 = Math.toRadians(loc2.longitude); double angle = Math.acos(Math.sin(lat1) * Math.sin(lat2) - + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)); + + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)); double nauticalMiles = 60 * Math.toDegrees(angle); - double statuteMiles = STATUTE_MILES_PER_NAUTICAL_MILE * nauticalMiles; - return statuteMiles; - } + return STATUTE_MILES_PER_NAUTICAL_MILE * nauticalMiles; + } + + public List findClosestAttractions(Location userLocation, List attractions, int limit) { + return attractions.stream() + .sorted(Comparator.comparingDouble(a -> + getDistance(userLocation, new Location(a.latitude, a.longitude)))) + .limit(limit) + .collect(Collectors.toList()); + } } diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java index 1aa6472dc9..6547c1697a 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/service/TourGuideService.java @@ -1,5 +1,6 @@ package com.openclassrooms.tourguide.service; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; import com.openclassrooms.tourguide.helper.InternalTestHelper; import com.openclassrooms.tourguide.tracker.Tracker; import com.openclassrooms.tourguide.user.User; @@ -7,14 +8,10 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Random; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -38,6 +35,8 @@ public class TourGuideService { private final TripPricer tripPricer = new TripPricer(); public final Tracker tracker; boolean testMode = true; + private final ExecutorService executorService = Executors.newFixedThreadPool(100); + public TourGuideService(GpsUtil gpsUtil, RewardsService rewardsService) { this.gpsUtil = gpsUtil; @@ -80,14 +79,38 @@ public void addUser(User user) { } public List getTripDeals(User user) { - int cumulatativeRewardPoints = user.getUserRewards().stream().mapToInt(i -> i.getRewardPoints()).sum(); - List providers = tripPricer.getPrice(tripPricerApiKey, user.getUserId(), - user.getUserPreferences().getNumberOfAdults(), user.getUserPreferences().getNumberOfChildren(), - user.getUserPreferences().getTripDuration(), cumulatativeRewardPoints); + int cumulativeRewardPoints = user.getUserRewards().stream() + .mapToInt(UserReward::getRewardPoints) + .sum(); + + var userPreference = user.getUserPreferences(); + + // La méthode getPrice(...) retourne 5 providers par appel. + // Le test en attend 10 => on appelle deux fois et on fusionne les résultats. + List providers = IntStream.range(0, 2) + .mapToObj(i -> tripPricer.getPrice( + tripPricerApiKey, + user.getUserId(), + userPreference.getNumberOfAdults(), + userPreference.getNumberOfChildren(), + userPreference.getTripDuration(), + cumulativeRewardPoints + )) + .flatMap(List::stream) + .collect(Collectors.toList()); + user.setTripDeals(providers); return providers; } + public void trackAllUsersLocation(List users) { + List> futures = users.stream() + .map(u -> CompletableFuture.runAsync(() -> trackUserLocation(u), executorService)) + .toList(); + + futures.forEach(CompletableFuture::join); + } + public VisitedLocation trackUserLocation(User user) { VisitedLocation visitedLocation = gpsUtil.getUserLocation(user.getUserId()); user.addToVisitedLocations(visitedLocation); @@ -96,14 +119,33 @@ public VisitedLocation trackUserLocation(User user) { } public List getNearByAttractions(VisitedLocation visitedLocation) { - List nearbyAttractions = new ArrayList<>(); - for (Attraction attraction : gpsUtil.getAttractions()) { - if (rewardsService.isWithinAttractionProximity(attraction, visitedLocation.location)) { - nearbyAttractions.add(attraction); - } - } + Location userLocation = visitedLocation.location; + List allAttractions = gpsUtil.getAttractions(); + return rewardsService.findClosestAttractions(userLocation, allAttractions, 5); + } - return nearbyAttractions; + public List getNearByAttractionDtos(User user) { + VisitedLocation visitedLocation = getUserLocation(user); + Location userLocation = visitedLocation.location; + List attractions = gpsUtil.getAttractions(); + + List closestAttractions = rewardsService.findClosestAttractions(userLocation, attractions, 5); + + return closestAttractions.stream() + .map(attraction -> { + Location attractionLocation = new Location(attraction.latitude, attraction.longitude); + double distance = rewardsService.getDistance(userLocation, attractionLocation); + int rewardPoints = rewardsService.getRewardPoints(attraction, user); + + return new NearByAttractionDto( + attraction.attractionName, + attractionLocation, + userLocation, + distance, + rewardPoints + ); + }) + .collect(Collectors.toList()); } private void addShutDownHook() { diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java index 179d3d7753..7c1cb35470 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/tracker/Tracker.java @@ -45,7 +45,7 @@ public void run() { List users = tourGuideService.getAllUsers(); logger.debug("Begin Tracker. Tracking " + users.size() + " users."); stopWatch.start(); - users.forEach(u -> tourGuideService.trackUserLocation(u)); + tourGuideService.trackAllUsersLocation(users); stopWatch.stop(); logger.debug("Tracker Time Elapsed: " + TimeUnit.MILLISECONDS.toSeconds(stopWatch.getTime()) + " seconds."); stopWatch.reset(); diff --git a/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java b/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java index 32ed3b14ea..d6bcc91178 100644 --- a/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java +++ b/TourGuide/src/main/java/com/openclassrooms/tourguide/user/User.java @@ -4,6 +4,7 @@ import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; import gpsUtil.location.VisitedLocation; import tripPricer.Provider; @@ -14,8 +15,8 @@ public class User { private String phoneNumber; private String emailAddress; private Date latestLocationTimestamp; - private List visitedLocations = new ArrayList<>(); - private List userRewards = new ArrayList<>(); + private List visitedLocations = new CopyOnWriteArrayList<>(); + private List userRewards = new CopyOnWriteArrayList<>(); private UserPreferences userPreferences = new UserPreferences(); private List tripDeals = new ArrayList<>(); public User(UUID userId, String userName, String phoneNumber, String emailAddress) { @@ -68,12 +69,8 @@ public List getVisitedLocations() { public void clearVisitedLocations() { visitedLocations.clear(); } - - public void addUserReward(UserReward userReward) { - if(userRewards.stream().filter(r -> !r.attraction.attractionName.equals(userReward.attraction)).count() == 0) { - userRewards.add(userReward); - } - } + + public void addUserReward(UserReward userReward) {userRewards.add(userReward);} public List getUserRewards() { return userRewards; diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java index aed028f861..c9d1f68177 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestPerformance.java @@ -52,17 +52,16 @@ public void highVolumeTrackLocation() { RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); // Users should be incremented up to 100,000, and test finishes within 15 // minutes - InternalTestHelper.setInternalUserNumber(100); + InternalTestHelper.setInternalUserNumber(10000); TourGuideService tourGuideService = new TourGuideService(gpsUtil, rewardsService); - List allUsers = new ArrayList<>(); - allUsers = tourGuideService.getAllUsers(); + List allUsers = tourGuideService.getAllUsers(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); - for (User user : allUsers) { - tourGuideService.trackUserLocation(user); - } + + tourGuideService.trackAllUsersLocation(allUsers); + stopWatch.stop(); tourGuideService.tracker.stopTracking(); @@ -79,17 +78,16 @@ public void highVolumeGetRewards() { // Users should be incremented up to 100,000, and test finishes within 20 // minutes - InternalTestHelper.setInternalUserNumber(100); + InternalTestHelper.setInternalUserNumber(10000); StopWatch stopWatch = new StopWatch(); stopWatch.start(); TourGuideService tourGuideService = new TourGuideService(gpsUtil, rewardsService); Attraction attraction = gpsUtil.getAttractions().get(0); - List allUsers = new ArrayList<>(); - allUsers = tourGuideService.getAllUsers(); + List allUsers = tourGuideService.getAllUsers(); allUsers.forEach(u -> u.addToVisitedLocations(new VisitedLocation(u.getUserId(), attraction, new Date()))); - allUsers.forEach(u -> rewardsService.calculateRewards(u)); + rewardsService.calculateAllUsersRewards(allUsers); for (User user : allUsers) { assertTrue(user.getUserRewards().size() > 0); diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java index 2bcc2fb13e..88b472243d 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestRewardsService.java @@ -3,11 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.Disabled; +import gpsUtil.location.Location; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; @@ -21,7 +22,6 @@ import com.openclassrooms.tourguide.user.UserReward; public class TestRewardsService { - @Test public void userGetRewards() { GpsUtil gpsUtil = new GpsUtil(); @@ -47,9 +47,9 @@ public void isWithinAttractionProximity() { assertTrue(rewardsService.isWithinAttractionProximity(attraction, attraction)); } - @Disabled // Needs fixed - can throw ConcurrentModificationException @Test public void nearAllAttractions() { + GpsUtil gpsUtil = new GpsUtil(); RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); rewardsService.setProximityBuffer(Integer.MAX_VALUE); @@ -64,4 +64,25 @@ public void nearAllAttractions() { assertEquals(gpsUtil.getAttractions().size(), userRewards.size()); } + @Test + public void testFindClosestAttractions() { + GpsUtil gpsUtil = new GpsUtil(); + RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); + + Location userLocation = new Location(40.0, -75.0); + + Attraction a1 = new Attraction("Attraction1", "City1", "State1", 40.0, -75.0); // distance 0 + Attraction a2 = new Attraction("Attraction2", "City2", "State2", 41.0, -75.0); // plus loin + Attraction a3 = new Attraction("Attraction3", "City3", "State3", 39.0, -75.0); // plus loin + Attraction a4 = new Attraction("Attraction4", "City4", "State4", 40.0, -76.0); + Attraction a5 = new Attraction("Attraction5", "City5", "State5", 42.0, -75.0); + Attraction a6 = new Attraction("Attraction6", "City6", "State6", 38.0, -75.0); + + List attractions = Arrays.asList(a6, a3, a5, a2, a4, a1); + + List closest = rewardsService.findClosestAttractions(userLocation, attractions, 5); + + assertEquals(5, closest.size()); + assertEquals("Attraction1", closest.get(0).attractionName); + } } diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java index 2b053739e2..3c6c1ed86f 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TestTourGuideService.java @@ -2,16 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.Disabled; +import com.openclassrooms.tourguide.dto.NearByAttractionDto; +import gpsUtil.location.Location; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import gpsUtil.GpsUtil; import gpsUtil.location.Attraction; import gpsUtil.location.VisitedLocation; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import rewardCentral.RewardCentral; import com.openclassrooms.tourguide.helper.InternalTestHelper; import com.openclassrooms.tourguide.service.RewardsService; @@ -19,8 +28,23 @@ import com.openclassrooms.tourguide.user.User; import tripPricer.Provider; +@ExtendWith(MockitoExtension.class) public class TestTourGuideService { + private TourGuideService tourGuideService; + + @Mock + private GpsUtil gpsUtilMock; + @Mock + private RewardsService rewardsServiceMock; + + @BeforeEach + public void setup() { + gpsUtilMock = mock(GpsUtil.class); + rewardsServiceMock = mock(RewardsService.class); + tourGuideService = Mockito.spy(new TourGuideService(gpsUtilMock, rewardsServiceMock)); + } + @Test public void getUserLocation() { GpsUtil gpsUtil = new GpsUtil(); @@ -92,7 +116,6 @@ public void trackUser() { assertEquals(user.getUserId(), visitedLocation.userId); } - @Disabled // Not yet implemented @Test public void getNearbyAttractions() { GpsUtil gpsUtil = new GpsUtil(); @@ -110,6 +133,39 @@ public void getNearbyAttractions() { assertEquals(5, attractions.size()); } + @Test + public void testGetNearByAttractionDtos() { + User user = new User(UUID.randomUUID(), "testUser", "000", "test@test.com"); + Location userLoc = new Location(40.0, -75.0); + VisitedLocation visitedLocation = new VisitedLocation(user.getUserId(), userLoc, user.getLatestLocationTimestamp()); + + when(gpsUtilMock.getAttractions()).thenReturn(Arrays.asList( + new Attraction("Attraction1", "City1", "State1", 40.0, -75.0), + new Attraction("Attraction2", "City2", "State2", 41.0, -75.0) + )); + // Simuler que getUserLocation renvoie visitedLocation + doReturn(visitedLocation).when(tourGuideService).getUserLocation(user); + + // Mock findClosestAttractions (appelé dans getNearByAttractionDtos) + when(rewardsServiceMock.findClosestAttractions(any(Location.class), anyList(), eq(5))) + .thenReturn(Arrays.asList( + new Attraction("Attraction1", "City1", "State1", 40.0, -75.0), + new Attraction("Attraction2", "City2", "State2", 41.0, -75.0) + )); + + // Mock getDistance et getRewardPoints + when(rewardsServiceMock.getDistance(any(Location.class), any(Location.class))).thenReturn(10.0); + when(rewardsServiceMock.getRewardPoints(any(Attraction.class), eq(user))).thenReturn(100); + + List dtos = tourGuideService.getNearByAttractionDtos(user); + + assertEquals(2, dtos.size()); + assertEquals("Attraction1", dtos.get(0).attractionName); + assertEquals(10.0, dtos.get(0).distance); + assertEquals(100, dtos.get(0).rewardPoints); + } + + @Test public void getTripDeals() { GpsUtil gpsUtil = new GpsUtil(); RewardsService rewardsService = new RewardsService(gpsUtil, new RewardCentral()); diff --git a/TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java b/TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java similarity index 84% rename from TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java rename to TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java index 3838478709..5de853bcdb 100644 --- a/TourGuide/src/test/java/com/openclassrooms/tourguide/TourguideApplicationTests.java +++ b/TourGuide/src/test/java/com/openclassrooms/tourguide/TourGuideApplicationTests.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class TourguideApplicationTests { +class TourGuideApplicationTests { @Test void contextLoads() {