diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5dbadb93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:22 +ARG JAR_FILE=out/artifacts/coding_project_jar/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 5b53a0b1..d32b96c2 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,29 @@ -# Harbor Take Home Project -Welcome to the Harbor take home project. We hope this is a good opportunity for you to showcase your skills. +## MVP Features supported: +- User is able to create, view, update, delete his profile +- User is able to create, view , modify and delete his availability +- User is able to share his entire availability to other users +- User is able to see all overlaps with any set of registered users +- user is able to create, view, update, cancel booking meeting -## The Challenge +## Test Coverage +- Added unit test for userService.(Skipped other classes in the interest of time) +- Exhaustive Integration Test Coverage using RestTemplate for all features except BookingController. +- Integration test coverage ensures working Apis and its correctness -Build us a REST API for calendly. Remember to support +## Assumptions +-Using InMemory Database for persisting data. +-All users register on the application to be able to use the features +- Have skipped some validation and incremental logic in some parts of application.Will keep on iterating on the same. -- Setting own availability -- Showing own availability -- Finding overlap in schedule between 2 users +## Future Improvements +- Add logging framework +- Emit appropriate metrics +- Integrate Swagger +- Integrate Java Cods Coverage -It is up to you what else to support. -## Expectations -We care about -- Have you thought through what a good MVP looks like? Does your API support that? -- What trade-offs are you making in your design? -- Working code - we should be able to pull and hit the code locally. Bonus points if deployed somewhere. -- Any good engineer will make hacks when necessary - what are your hacks and why? -We don't care about -- Authentication -- UI -- Perfection - good and working quickly is better - -It is up to you how much time you want to spend on this project. There are likely diminishing returns as the time spent goes up. - -## Submission - -Please fork this repository and reach out to Prakash when finished. - -## Next Steps - -After submission, we will conduct a 30 to 60 minute code review in person. We will ask you about your thinking and design choices. diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..8c1eb7bf --- /dev/null +++ b/pom.xml @@ -0,0 +1,85 @@ + + +4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + +org.p0 +calendly +0.0.2-SNAPSHOT +calendly +Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + io.rest-assured + rest-assured + 5.5.0 + test + + + junit + junit + test + + + + org.springframework.boot + spring-boot-starter-validation + 3.0.2 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/org/p0/calendly/CalendlyApplication.java b/src/main/java/org/p0/calendly/CalendlyApplication.java new file mode 100644 index 00000000..da1be346 --- /dev/null +++ b/src/main/java/org/p0/calendly/CalendlyApplication.java @@ -0,0 +1,14 @@ +package org.p0.calendly; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CalendlyApplication { + + public static void main(String[] args) { + SpringApplication.run(CalendlyApplication.class, args); + System.out.println("Hello"); + } + +} diff --git a/src/main/java/org/p0/calendly/controllers/BookingController.java b/src/main/java/org/p0/calendly/controllers/BookingController.java new file mode 100644 index 00000000..f4a2a465 --- /dev/null +++ b/src/main/java/org/p0/calendly/controllers/BookingController.java @@ -0,0 +1,67 @@ +package org.p0.calendly.controllers; + +import lombok.NonNull; +import org.p0.calendly.dtos.*; +import org.p0.calendly.models.Booking; +import org.p0.calendly.services.BookingManagementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/book") +public class BookingController { + + + @Autowired + private BookingManagementService meetingManagementService; + + // create booking + @PostMapping + public ResponseEntity createBooking(@RequestBody @NonNull MeetingBookingRequest meetingBookingRequest) { + // todo: input validations + String response = meetingManagementService.createBooking(meetingBookingRequest); + + return ResponseEntity.status(201).body(response); + } + + // Get booking by bookingID + @GetMapping("/{id}") + public ResponseEntity getBookingById(@PathVariable @NonNull String id) { + Booking response = null; + // todo: input validations + response= meetingManagementService.getBookingById(id);; + + return ResponseEntity.status(200).body(response); + } + + // Get booking by userId + @GetMapping("/user/{id}") + public ResponseEntity> getBookingByuserId(@PathVariable @NonNull String id) { + List response; + + response= meetingManagementService.getBookingByuserId(id); + + return ResponseEntity.status(200).body(response); + } + + // Update booking by bookingId + @PutMapping("/{id}") + public ResponseEntity updateBooking(@PathVariable @NonNull String id, @RequestBody @NonNull Booking bookingDetails) { + Booking response = null; + + response= meetingManagementService.updateBooking(id, bookingDetails); + + + return ResponseEntity.ok(response); + } + + // Delete booking by bookingId + @DeleteMapping("/{id}") + public ResponseEntity deleteBooking(@PathVariable String id) { + meetingManagementService.deleteBooking(id); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/p0/calendly/controllers/OverlapController.java b/src/main/java/org/p0/calendly/controllers/OverlapController.java new file mode 100644 index 00000000..26d0ce31 --- /dev/null +++ b/src/main/java/org/p0/calendly/controllers/OverlapController.java @@ -0,0 +1,39 @@ +package org.p0.calendly.controllers; + +import lombok.NonNull; +import org.p0.calendly.dtos.ScheduleOverlapRequest; +import org.p0.calendly.dtos.ScheduleOverlapResponse; +import org.p0.calendly.exceptions.NotEnoughUsersException; +import org.p0.calendly.services.AvailabilityOverlapService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/overlap") +public class OverlapController { + + + @Autowired + private AvailabilityOverlapService availabilityOverlapService; + + // get overlaps for comma separated userIds + @GetMapping("/ids={id}") + public ResponseEntity getOverlaps(@PathVariable @NonNull String id) { + // todo: validate input + List scheduleOverlapRequest = Arrays.asList(id.split(",", -1)); + + ScheduleOverlapResponse response = new ScheduleOverlapResponse(); + try { + response = availabilityOverlapService.getUserOverlaps(scheduleOverlapRequest); + } + catch (NotEnoughUsersException e) + { + return ResponseEntity.status(400).build(); + } + return ResponseEntity.status(200).body(response); + } +} diff --git a/src/main/java/org/p0/calendly/controllers/UserAvailabilityController.java b/src/main/java/org/p0/calendly/controllers/UserAvailabilityController.java new file mode 100644 index 00000000..fa7e2158 --- /dev/null +++ b/src/main/java/org/p0/calendly/controllers/UserAvailabilityController.java @@ -0,0 +1,53 @@ +package org.p0.calendly.controllers; + + +import lombok.NonNull; +import org.p0.calendly.dtos.AvailabilityResponse; +import org.p0.calendly.dtos.Availabilityrequest; +import org.p0.calendly.exceptions.UserAlreadyExistsException; +import org.p0.calendly.exceptions.UserNotFoundException; +import org.p0.calendly.models.User; +import org.p0.calendly.services.UserAvailabilityService; +import org.p0.calendly.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/availability") +public class UserAvailabilityController { + + + @Autowired + private UserAvailabilityService userAvailabilityService; + + // Create user availability + @PostMapping + public ResponseEntity createUserAvailability(@RequestBody @NonNull Availabilityrequest availabilityrequest) { + + User response = null; + try { + userAvailabilityService.createUserAvailabilty(availabilityrequest); + } + catch (UserNotFoundException e) + { + return ResponseEntity.status(404).build(); + } + return ResponseEntity.status(201).build(); + } + + // Get user availability + @GetMapping("/{id}") + public ResponseEntity getUserAvailability(@PathVariable @NonNull String id) { + AvailabilityResponse response = new AvailabilityResponse(); + try { + response= userAvailabilityService.getUserAvailability(id); + } + catch (UserNotFoundException e) + { + return ResponseEntity.status(404).body(response); + } + return ResponseEntity.status(200).body(response); + } +} diff --git a/src/main/java/org/p0/calendly/controllers/UserProfileController.java b/src/main/java/org/p0/calendly/controllers/UserProfileController.java new file mode 100644 index 00000000..a81b8ff4 --- /dev/null +++ b/src/main/java/org/p0/calendly/controllers/UserProfileController.java @@ -0,0 +1,75 @@ +package org.p0.calendly.controllers; + + +import lombok.NonNull; +import org.p0.calendly.exceptions.UserAlreadyExistsException; +import org.p0.calendly.exceptions.UserNotFoundException; +import org.p0.calendly.models.User; +import org.p0.calendly.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/user") +public class UserProfileController { + + + @Autowired + private UserService userService; + + // Create user + @PostMapping + public ResponseEntity createUser(@RequestBody @NonNull User user) { + + User response = null; + try { + response= userService.createUser(user); + } + catch (UserAlreadyExistsException e) + { + //status 409 : conflict + return ResponseEntity.status(409).body(response); + } + return ResponseEntity.status(201).body(response); + } + + // Get user by ID + @GetMapping("/{id}") + public ResponseEntity getUserById(@PathVariable @NonNull String id) { + User response = null; + try { + response= userService.getUserById(id); + } + catch (UserNotFoundException e) + { + //status 404 : not found + return ResponseEntity.status(404).build(); + } + return ResponseEntity.status(200).body(response); + + } + + // Update user + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable @NonNull String id, @RequestBody @NonNull User userDetails) { + User response = null; + + try { + response= userService.updateUser(id, userDetails); + } + catch (UserNotFoundException e) + { + //status 404 : conflict + return ResponseEntity.status(404).build(); + } + return ResponseEntity.ok(response); + } + + // Delete user + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable String id) { + userService.deleteUser(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/p0/calendly/dtos/AvailabilityResponse.java b/src/main/java/org/p0/calendly/dtos/AvailabilityResponse.java new file mode 100644 index 00000000..340b0e91 --- /dev/null +++ b/src/main/java/org/p0/calendly/dtos/AvailabilityResponse.java @@ -0,0 +1,15 @@ +package org.p0.calendly.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.p0.calendly.models.Schedule; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class AvailabilityResponse { + private List scheduleList; +} diff --git a/src/main/java/org/p0/calendly/dtos/Availabilityrequest.java b/src/main/java/org/p0/calendly/dtos/Availabilityrequest.java new file mode 100644 index 00000000..e6aacfee --- /dev/null +++ b/src/main/java/org/p0/calendly/dtos/Availabilityrequest.java @@ -0,0 +1,14 @@ +package org.p0.calendly.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.p0.calendly.models.Schedule; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class Availabilityrequest { + private String userId; + private List scheduleList; +} diff --git a/src/main/java/org/p0/calendly/dtos/MeetingBookingRequest.java b/src/main/java/org/p0/calendly/dtos/MeetingBookingRequest.java new file mode 100644 index 00000000..935fe520 --- /dev/null +++ b/src/main/java/org/p0/calendly/dtos/MeetingBookingRequest.java @@ -0,0 +1,20 @@ +package org.p0.calendly.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.p0.calendly.models.enums.RecurrenceType; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class MeetingBookingRequest { + private String organizer; + private long startDate; + private int startTime; + private int endTime; + private RecurrenceType recurrenceType; + private Long endDate; + private List audience; + //metadata +} \ No newline at end of file diff --git a/src/main/java/org/p0/calendly/dtos/ScheduleOverlapRequest.java b/src/main/java/org/p0/calendly/dtos/ScheduleOverlapRequest.java new file mode 100644 index 00000000..362fe4ea --- /dev/null +++ b/src/main/java/org/p0/calendly/dtos/ScheduleOverlapRequest.java @@ -0,0 +1,12 @@ +package org.p0.calendly.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class ScheduleOverlapRequest { + private List users; +} diff --git a/src/main/java/org/p0/calendly/dtos/ScheduleOverlapResponse.java b/src/main/java/org/p0/calendly/dtos/ScheduleOverlapResponse.java new file mode 100644 index 00000000..e1dbd68b --- /dev/null +++ b/src/main/java/org/p0/calendly/dtos/ScheduleOverlapResponse.java @@ -0,0 +1,14 @@ +package org.p0.calendly.dtos; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.TreeMap; + +@NoArgsConstructor +@Getter +@AllArgsConstructor +public class ScheduleOverlapResponse { + private TreeMap overlaps; +} diff --git a/src/main/java/org/p0/calendly/exceptions/NotEnoughUsersException.java b/src/main/java/org/p0/calendly/exceptions/NotEnoughUsersException.java new file mode 100644 index 00000000..96fac59c --- /dev/null +++ b/src/main/java/org/p0/calendly/exceptions/NotEnoughUsersException.java @@ -0,0 +1,4 @@ +package org.p0.calendly.exceptions; + +public class NotEnoughUsersException extends RuntimeException { +} diff --git a/src/main/java/org/p0/calendly/exceptions/UserAlreadyExistsException.java b/src/main/java/org/p0/calendly/exceptions/UserAlreadyExistsException.java new file mode 100644 index 00000000..83a5ddf6 --- /dev/null +++ b/src/main/java/org/p0/calendly/exceptions/UserAlreadyExistsException.java @@ -0,0 +1,4 @@ +package org.p0.calendly.exceptions; + +public class UserAlreadyExistsException extends RuntimeException{ +} diff --git a/src/main/java/org/p0/calendly/exceptions/UserNotFoundException.java b/src/main/java/org/p0/calendly/exceptions/UserNotFoundException.java new file mode 100644 index 00000000..3125ec5e --- /dev/null +++ b/src/main/java/org/p0/calendly/exceptions/UserNotFoundException.java @@ -0,0 +1,4 @@ +package org.p0.calendly.exceptions; + +public class UserNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/org/p0/calendly/models/Booking.java b/src/main/java/org/p0/calendly/models/Booking.java new file mode 100644 index 00000000..94fe3103 --- /dev/null +++ b/src/main/java/org/p0/calendly/models/Booking.java @@ -0,0 +1,47 @@ +package org.p0.calendly.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; + +@AllArgsConstructor +@Getter +public class Booking { + private String bookingId; + + private String linkedBookingId; + @Setter + private String organizer; + @Setter + private long startTime; + @Setter + private long endTime; + @Setter + private List audience; + //metadata + + public Booking(@NonNull String organizer, long startTime, long endTime, @NonNull List audience,String linkedBookingId) + { + this.bookingId = this.setUniqueBookingId(); + this.linkedBookingId = linkedBookingId ==null ? setUniquelinkedBookingId() : linkedBookingId; + this.audience = audience; + this.startTime = startTime; + this.endTime = endTime; + this.organizer = organizer; + } + + + private String setUniqueBookingId() + { + return UUID.randomUUID().toString(); + } + + private String setUniquelinkedBookingId() + { + return UUID.randomUUID().toString(); + } +} diff --git a/src/main/java/org/p0/calendly/models/Schedule.java b/src/main/java/org/p0/calendly/models/Schedule.java new file mode 100644 index 00000000..5687dbb8 --- /dev/null +++ b/src/main/java/org/p0/calendly/models/Schedule.java @@ -0,0 +1,19 @@ +package org.p0.calendly.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import org.p0.calendly.models.enums.RecurrenceType; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class Schedule { + private long startDate; + private long startTime; + private long endTime; + private RecurrenceType recurrenceType; + private Long endDate; + //metadata +} diff --git a/src/main/java/org/p0/calendly/models/User.java b/src/main/java/org/p0/calendly/models/User.java new file mode 100644 index 00000000..3671d21e --- /dev/null +++ b/src/main/java/org/p0/calendly/models/User.java @@ -0,0 +1,25 @@ +package org.p0.calendly.models; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Getter +@AllArgsConstructor +public class User { + private String id; + @Setter + private String firstName; + @Setter + private String lastName; + @Setter + private String email; + + public void setUniqueId() + { + this.id = UUID.randomUUID().toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/p0/calendly/models/enums/RecurrenceType.java b/src/main/java/org/p0/calendly/models/enums/RecurrenceType.java new file mode 100644 index 00000000..ffaee912 --- /dev/null +++ b/src/main/java/org/p0/calendly/models/enums/RecurrenceType.java @@ -0,0 +1,8 @@ +package org.p0.calendly.models.enums; + +public enum RecurrenceType { + NONE, + WEEKLY, + MONTHLY, + YEARLY +} diff --git a/src/main/java/org/p0/calendly/services/AvailabilityOverlapService.java b/src/main/java/org/p0/calendly/services/AvailabilityOverlapService.java new file mode 100644 index 00000000..a35de0a0 --- /dev/null +++ b/src/main/java/org/p0/calendly/services/AvailabilityOverlapService.java @@ -0,0 +1,45 @@ +package org.p0.calendly.services; + +import lombok.NonNull; +import org.p0.calendly.dtos.ScheduleOverlapRequest; +import org.p0.calendly.dtos.ScheduleOverlapResponse; +import org.p0.calendly.exceptions.NotEnoughUsersException; +import org.p0.calendly.strategies.OverlapStrategy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.TreeMap; + +@Service +public class AvailabilityOverlapService { + @Autowired + private UserAvailabilityService userAvailabilityService; + + @Autowired + private OverlapStrategy overlapStrategy; + + public ScheduleOverlapResponse getUserOverlaps(@NonNull List scheduleOverlapRequest) { + + if(scheduleOverlapRequest == null || scheduleOverlapRequest.size() < 2) + throw new NotEnoughUsersException(); + + return getOverlaps(scheduleOverlapRequest); + } + + private ScheduleOverlapResponse getOverlaps(List users) + { + // Todo: check user validation + String userId = users.get(0); + + TreeMap userAvailability = userAvailabilityService.getAvailability(userId); + + for (int i=1;i neighbourAvailability = userAvailabilityService.getAvailability(users.get(i)); + userAvailability = overlapStrategy.findOverlaps(userAvailability, neighbourAvailability); + } + + return new ScheduleOverlapResponse(userAvailability); + } +} diff --git a/src/main/java/org/p0/calendly/services/BookingManagementService.java b/src/main/java/org/p0/calendly/services/BookingManagementService.java new file mode 100644 index 00000000..67c325bc --- /dev/null +++ b/src/main/java/org/p0/calendly/services/BookingManagementService.java @@ -0,0 +1,178 @@ +package org.p0.calendly.services; + +import lombok.NonNull; +import org.p0.calendly.dtos.*; +import org.p0.calendly.models.Booking; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class BookingManagementService { + private Map bookingMap = new HashMap<>(); + private Map> userBookingMap = new HashMap<>(); + private Map> recurringBookingMap = new HashMap<>(); + + @Autowired + private UserAvailabilityService userAvailabilityService; + + public String createBooking(@NonNull MeetingBookingRequest meetingBookingRequest) { + return book(meetingBookingRequest); + } + + public Booking getBookingById(@NonNull String id) { + return bookingMap.get(id); + } + + public List getBookingByuserId(@NonNull String id) { + List bookingIds = userBookingMap.get(id); + List bookingList = new ArrayList<>(); + for(String bookingId : bookingIds) + { + bookingList.add(bookingMap.get(bookingId)); + } + return bookingList; + } + + public Booking updateBooking(@NonNull String id, @NonNull Booking bookingDetails) { + if(!bookingMap.containsKey(id)) + { + // todo : BookingNOTFOUNDEXCEPTION + } + + + Booking booking = bookingMap.get(id); + booking.setOrganizer(bookingDetails.getOrganizer()); + booking.setStartTime(bookingDetails.getStartTime()); + booking.setEndTime(bookingDetails.getEndTime()); + booking.setAudience(bookingDetails.getAudience()); + bookingMap.put(id,booking); + // Todo : Update availability + return booking; + } + + public void deleteBooking(String id) { + bookingMap.remove(id); + // Todo : Update availability + } + + + + + private String book(MeetingBookingRequest request) + { + Long startDate = request.getStartDate(); + Long startTime = Long.valueOf(request.getStartTime()); + Long endTime = Long.valueOf(request.getEndTime()); + Long endDate = request.getEndDate(); + + switch (request.getRecurrenceType()) + { + case NONE ->{ + if(userAvailabilityService.isSlotAvailable(request.getOrganizer(), startDate+startTime, + startDate + endTime)) + { + userAvailabilityService.updateAvailability(request.getOrganizer(), startDate+startTime, + startDate + endTime); + + Booking booking = new Booking(request.getOrganizer(), + startDate+startTime, + startDate+endTime,request.getAudience(), null); + bookingMap.put(booking.getBookingId(), booking); + + //update recurringmap + if(recurringBookingMap.containsKey(booking.getLinkedBookingId())) + { + List bookingIds = recurringBookingMap.get(booking.getLinkedBookingId()); + bookingIds.add(booking.getBookingId()); + recurringBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + else + { + List bookingIds = new ArrayList<>(); + bookingIds.add(booking.getBookingId()); + recurringBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + + //update usermap + if(userBookingMap.containsKey(booking.getOrganizer())) + { + List bookingIds = userBookingMap.get(booking.getOrganizer()); + bookingIds.add(booking.getBookingId()); + userBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + else + { + List bookingIds = new ArrayList<>(); + bookingIds.add(booking.getBookingId()); + userBookingMap.put(booking.getOrganizer(), bookingIds); + } + + return booking.getBookingId(); + } + + } + + case WEEKLY -> { + Long weeklyEpoch =Long.valueOf(7* 86400000); + Long tempStartDate = startDate; + String linkedBookingId = null; + while(endDate <= startDate) + { + + if(userAvailabilityService.isSlotAvailable(request.getOrganizer(), startDate+startTime, + startDate + endTime)) + { + userAvailabilityService.updateAvailability(request.getOrganizer(), startDate+startTime, + startDate + endTime); + Booking booking = new Booking(request.getOrganizer(), + startDate+startTime,startDate+endTime,request.getAudience(),linkedBookingId); + bookingMap.put(booking.getBookingId(), booking); + + //update recurringmap + if(recurringBookingMap.containsKey(booking.getLinkedBookingId())) + { + List bookingIds = recurringBookingMap.get(booking.getLinkedBookingId()); + bookingIds.add(booking.getBookingId()); + recurringBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + else + { + List bookingIds = new ArrayList<>(); + bookingIds.add(booking.getBookingId()); + recurringBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + + //update usermap + if(userBookingMap.containsKey(booking.getOrganizer())) + { + List bookingIds = userBookingMap.get(booking.getOrganizer()); + bookingIds.add(booking.getBookingId()); + userBookingMap.put(booking.getLinkedBookingId(), bookingIds); + } + else + { + List bookingIds = new ArrayList<>(); + bookingIds.add(booking.getBookingId()); + userBookingMap.put(booking.getOrganizer(), bookingIds); + } + linkedBookingId = booking.getLinkedBookingId(); + } + tempStartDate +=weeklyEpoch; + } + return linkedBookingId; + } + case MONTHLY -> { + //TOdo: add monthly + } + case YEARLY -> { + //TOdo: add yearly + } + } + +//Handle using NO_SLOT_AVAILABLE_EXCEPTION + return null; + } +} + diff --git a/src/main/java/org/p0/calendly/services/UserAvailabilityService.java b/src/main/java/org/p0/calendly/services/UserAvailabilityService.java new file mode 100644 index 00000000..b1f4700c --- /dev/null +++ b/src/main/java/org/p0/calendly/services/UserAvailabilityService.java @@ -0,0 +1,143 @@ +package org.p0.calendly.services; + +import org.p0.calendly.dtos.AvailabilityResponse; +import org.p0.calendly.dtos.Availabilityrequest; +import org.p0.calendly.exceptions.UserNotFoundException; +import org.p0.calendly.models.Schedule; +import org.p0.calendly.models.User; +import org.p0.calendly.models.enums.RecurrenceType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class UserAvailabilityService { + + private Map> userAvailabilityMap = new HashMap<>(); + + @Autowired + private UserService userService; + + // Create a new user + public void createUserAvailabilty(final Availabilityrequest availabilityrequest) { + + User user = userService.getUserById(availabilityrequest.getUserId()); + + userAvailabilityMap.put(availabilityrequest.getUserId(),addUserSchedule(availabilityrequest.getScheduleList())); + + } + public AvailabilityResponse getUserAvailability(String id) { + User user = userService.getUserById(id); + + return new AvailabilityResponse(convertTimestampToSchedule(userAvailabilityMap.get(id))); + } + + public TreeMap getAvailability(String id) + { + return userAvailabilityMap.get(id); + } + + public boolean isSlotAvailable(String userId, Long start, Long end) + { + if(!userAvailabilityMap.containsKey(userId)) + throw new UserNotFoundException(); + + TreeMap availability = userAvailabilityMap.get(userId); + + if(availability.containsKey(start) && availability.get(start) >= end) + return true; + if(availability.lowerKey(start) != null && availability.get(availability.lowerKey(start)) >= end) + return true; + + return false; + } + + public void updateAvailability(String userId, Long start, Long end) + { + TreeMap availability = userAvailabilityMap.get(userId); + + if(availability.containsKey(start) && availability.get(start) == end) + { + availability.remove(start); + + } + else if(availability.containsKey(start) && availability.get(start) > end) + { + availability.put(end,availability.get(start)); + availability.remove(start); + + } + else if(availability.lowerKey(start) != null && availability.get(availability.lowerKey(start)) == end) + { + availability.put(availability.lowerKey(start),start); + + } + else if(availability.lowerKey(start) != null && availability.get(availability.lowerKey(start)) >= end) + { + Long value = availability.get(availability.lowerKey(start)); + availability.put(availability.lowerKey(start),start); + availability.put(end,value); + + } + } + + private List convertTimestampToSchedule(TreeMap map) + { + Long weeklyEpoch =Long.valueOf(7* 86400000); + List scheduleList = new ArrayList<>(); + map.forEach((k,v) -> { + Long tempKey = k; + Long tempValue = v; + while(map.containsKey(tempKey + weeklyEpoch)) + { + tempValue = map.get(tempKey + weeklyEpoch); + map.remove(tempKey); + tempKey = tempKey + weeklyEpoch; + + } + + RecurrenceType type = tempKey != k ? RecurrenceType.WEEKLY : RecurrenceType.NONE; + scheduleList.add(new Schedule(tempKey - (tempKey% 86400000), + (tempKey % 86400000), (tempValue% 86400000), + type, tempValue - (tempValue % 86400000) + 86400000)); + }); + + return scheduleList; + } + + private TreeMap addUserSchedule(List scheduleList) + { + TreeMap scheduleMap = new TreeMap<>(); + + for(Schedule schedule : scheduleList) + { + Long startDate = schedule.getStartDate(); + Long startTime = Long.valueOf(schedule.getStartTime()); + Long endTime = Long.valueOf(schedule.getEndTime()); + Long endDate = schedule.getEndDate(); + + switch (schedule.getRecurrenceType()) + { + case NONE -> scheduleMap.put(startDate+startTime, startDate+endTime); + case WEEKLY -> { + Long weeklyEpoch =Long.valueOf(7* 86400000); + Long tempStartDate = startDate; + while(endDate <= startDate) + { + scheduleMap.put(startDate+startTime, startDate+endTime); + tempStartDate +=weeklyEpoch; + } + } + case MONTHLY -> { + //TOdo: add monthly + } + case YEARLY -> { + //TOdo: add yearly + } + } + } + + return scheduleMap; + } +} \ No newline at end of file diff --git a/src/main/java/org/p0/calendly/services/UserService.java b/src/main/java/org/p0/calendly/services/UserService.java new file mode 100644 index 00000000..8aa6516e --- /dev/null +++ b/src/main/java/org/p0/calendly/services/UserService.java @@ -0,0 +1,59 @@ +package org.p0.calendly.services; + +import org.p0.calendly.exceptions.UserAlreadyExistsException; +import org.p0.calendly.exceptions.UserNotFoundException; +import org.p0.calendly.models.User; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class UserService { + private Map userMap = new HashMap<>(); + + // Create a new user + public User createUser(User user) { + if(user.getId() == null || user.getId().isEmpty()) + user.setUniqueId(); + + if(userMap.containsKey(user.getId())) + { + throw new UserAlreadyExistsException(); + } + + userMap.put(user.getId(),user); + + return user; + } + + // Retrieve a single user by ID + public User getUserById(String id) { + if(userMap.containsKey(id)) + return userMap.get(id); + + throw new UserNotFoundException(); + + } + + // Update user details + public User updateUser(String id, User userDetails) { + if(!userMap.containsKey(id)) + { + throw new UserNotFoundException(); + } + + + User user = userMap.get(id); + user.setFirstName(userDetails.getFirstName()); + user.setLastName(userDetails.getLastName()); + user.setEmail(userDetails.getEmail()); + userMap.put(user.getId(),user); + return user; + } + + // Delete user by ID + public void deleteUser(String id) { + userMap.remove(id); + } +} \ No newline at end of file diff --git a/src/main/java/org/p0/calendly/strategies/DefaultOverlapStrategy.java b/src/main/java/org/p0/calendly/strategies/DefaultOverlapStrategy.java new file mode 100644 index 00000000..7b2d714d --- /dev/null +++ b/src/main/java/org/p0/calendly/strategies/DefaultOverlapStrategy.java @@ -0,0 +1,54 @@ +package org.p0.calendly.strategies; + +import org.springframework.stereotype.Service; + +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeMap; + +@Service +public class DefaultOverlapStrategy implements OverlapStrategy{ + + @Override + public TreeMap findOverlaps(TreeMap map1, TreeMap map2) { + TreeMap overlapMap = new TreeMap<>(); + + if(map1 == null || map1.isEmpty() || map2 == null || map2.isEmpty()) + return overlapMap; + + // Iterators for both maps + Iterator> iterator1 = map1.entrySet().iterator(); + Iterator> iterator2 = map2.entrySet().iterator(); + + // Get first intervals from both maps + Map.Entry interval1 = iterator1.hasNext() ? iterator1.next() : null; + Map.Entry interval2 = iterator2.hasNext() ? iterator2.next() : null; + + // Traverse both maps + while (interval1 != null && interval2 != null) { + long start1 = interval1.getKey(); + long end1 = interval1.getValue(); + long start2 = interval2.getKey(); + long end2 = interval2.getValue(); + + // Find the maximum of the starting points and the minimum of the ending points + long overlapStart = Math.max(start1, start2); + long overlapEnd = Math.min(end1, end2); + + // Check if there is an overlap + if (overlapStart <= overlapEnd) { + overlapMap.put(overlapStart, overlapEnd); + } + + // Move the iterator with the smaller ending point forward + if (end1 < end2) { + interval1 = iterator1.hasNext() ? iterator1.next() : null; + } else { + interval2 = iterator2.hasNext() ? iterator2.next() : null; + } + } + + return overlapMap; + } +} diff --git a/src/main/java/org/p0/calendly/strategies/OverlapStrategy.java b/src/main/java/org/p0/calendly/strategies/OverlapStrategy.java new file mode 100644 index 00000000..11ce5500 --- /dev/null +++ b/src/main/java/org/p0/calendly/strategies/OverlapStrategy.java @@ -0,0 +1,8 @@ +package org.p0.calendly.strategies; + +import java.util.TreeMap; + +public interface OverlapStrategy { + + public TreeMap findOverlaps(TreeMap u1, TreeMap u2); +} diff --git a/src/main/java/org/p0/calendly/utils/RestTemplateClient.java b/src/main/java/org/p0/calendly/utils/RestTemplateClient.java new file mode 100644 index 00000000..648edd1a --- /dev/null +++ b/src/main/java/org/p0/calendly/utils/RestTemplateClient.java @@ -0,0 +1,10 @@ +package org.p0.calendly.utils; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateClient extends RestTemplate { + +} diff --git a/src/test/java/ApiIntegrationTests/OverlapControllerIntegrationTests.java b/src/test/java/ApiIntegrationTests/OverlapControllerIntegrationTests.java new file mode 100644 index 00000000..68f2cf66 --- /dev/null +++ b/src/test/java/ApiIntegrationTests/OverlapControllerIntegrationTests.java @@ -0,0 +1,138 @@ +package ApiIntegrationTests; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.p0.calendly.CalendlyApplication; +import org.p0.calendly.dtos.Availabilityrequest; +import org.p0.calendly.dtos.ScheduleOverlapResponse; +import org.p0.calendly.models.Schedule; +import org.p0.calendly.models.User; +import org.p0.calendly.models.enums.RecurrenceType; +import org.p0.calendly.utils.RestTemplateClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.*; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = CalendlyApplication.class) + +public class OverlapControllerIntegrationTests { + + @LocalServerPort + private int port; + + + private String userBaseUrl, availabilityBaseUrl, overlapBaseUrl; + private String user1Id, user2Id, user3Id; + + @Autowired + private RestTemplateClient restTemplate; + + @BeforeEach + public void setUp() { + availabilityBaseUrl = "http://localhost:" + port + "/api/v1/availability"; + userBaseUrl = "http://localhost:" + port + "/api/v1/user"; + overlapBaseUrl = "http://localhost:" + port + "/api/v1/overlap"; + + // refractor setup method to use TESTUTILS for creating user and schedule + + //create user1 + User user1 = new User(null, "Elizabeth", "Swann", "elizabeth.swann@example.com"); + ResponseEntity response1 = restTemplate.postForEntity(userBaseUrl, user1, User.class); + user1Id = response1.getBody().getId().toString(); + + // First create availability for a user + // start date 1704844800000L = 2024/01/10 00:00:00 UTC + //end date 1704931200000L = 2024/01/11 00:00:00 UTC + Schedule schedule1 = new Schedule(1704844800000L, 4800000, 8400000, RecurrenceType.NONE, 1704931200000L); + List scheduleList1 = Arrays.asList(schedule1); + //Availabilityrequest availabilityRequest = new Availabilityrequest(user1Id, scheduleList); + restTemplate.postForEntity(availabilityBaseUrl, new Availabilityrequest(user1Id, scheduleList1), Void.class); + + + + //create user2 + User user2 = new User(null, "Amit", "G", "a.g@example.com"); + ResponseEntity response2 = restTemplate.postForEntity(userBaseUrl, user2, User.class); + user2Id = response2.getBody().getId().toString(); + + // First create availability for a user + // start date 1704844800000L = 2024/01/10 00:00:00 UTC + //end date 1704931200000L = 2024/01/11 00:00:00 UTC + Schedule schedule2 = new Schedule(1704844800000L, 3600000, 7200000, RecurrenceType.NONE, 1704931200000L); + List scheduleList2 = Arrays.asList(schedule2); + //Availabilityrequest availabilityRequest = new Availabilityrequest(id, scheduleList); + restTemplate.postForEntity(availabilityBaseUrl, new Availabilityrequest(user2Id, scheduleList2), Void.class); + + + + //create user3 + User user3 = new User(null, "Ekan", "S", "e.s@example.com"); + ResponseEntity response3 = restTemplate.postForEntity(userBaseUrl, user3, User.class); + user3Id = response3.getBody().getId().toString(); + + // First create availability for a user + // start date 1705708800000L = 2024/01/20 00:00:00 UTC + //end date 1705795200000L = 2024/01/21 00:00:00 UTC + Schedule schedule3 = new Schedule(1705708800000L, 3600000, 7200000, RecurrenceType.NONE, 1705795200000L); + List scheduleList3 = Arrays.asList(schedule3); + //Availabilityrequest availabilityRequest = new Availabilityrequest(id, scheduleList); + restTemplate.postForEntity(availabilityBaseUrl, new Availabilityrequest(user3Id, scheduleList3), Void.class); + + } + + // // Test Get User Availability with meaningful method name + @Test + public void getUserAvailability_ShouldReturn200_WhenUserExists() { + // Setup the ScheduleOverlapRequest with user IDs + String id = user1Id + "," + user2Id; + + // Now retrieve the availability for the user + ResponseEntity responseEntity = restTemplate.getForEntity(overlapBaseUrl + "/ids={id}", ScheduleOverlapResponse.class, id); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertEquals(1, responseEntity.getBody().getOverlaps().size()); + assertEquals(1704849600000L, responseEntity.getBody().getOverlaps().firstEntry().getKey()); + assertEquals(1704852000000L, responseEntity.getBody().getOverlaps().firstEntry().getValue()); + } + + // Test for NotEnoughUsersException + @Test + public void getOverlaps_ShouldReturn400_WhenNotEnoughUsersProvided() { + // Setup the ScheduleOverlapRequest with a single user (which should trigger NotEnoughUsersException) + String id = user1Id ; + + try { + // Now retrieve the availability for the user + ResponseEntity responseEntity = restTemplate.getForEntity(overlapBaseUrl + "/ids={id}", ScheduleOverlapResponse.class, id); + } catch (HttpClientErrorException ex) { + // Validate that a 400 Bad Request is returned + assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode()); + } + } + + @Test + public void getUserAvailability_ShouldReturnEmpty_WhenNoOverlaps() { + // Setup the ScheduleOverlapRequest with user IDs + String id = user1Id + "," + user3Id; + + // Now retrieve the availability for the user + ResponseEntity responseEntity = restTemplate.getForEntity(overlapBaseUrl + "/ids={id}", ScheduleOverlapResponse.class, id); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertEquals(0, responseEntity.getBody().getOverlaps().size()); + } +} \ No newline at end of file diff --git a/src/test/java/ApiIntegrationTests/UserAvailabilityControllerIntegrationTests.java b/src/test/java/ApiIntegrationTests/UserAvailabilityControllerIntegrationTests.java new file mode 100644 index 00000000..6334a250 --- /dev/null +++ b/src/test/java/ApiIntegrationTests/UserAvailabilityControllerIntegrationTests.java @@ -0,0 +1,115 @@ +package ApiIntegrationTests; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.p0.calendly.CalendlyApplication; +import org.p0.calendly.dtos.AvailabilityResponse; +import org.p0.calendly.dtos.Availabilityrequest; +import org.p0.calendly.models.Schedule; +import org.p0.calendly.models.User; +import org.p0.calendly.models.enums.RecurrenceType; +import org.p0.calendly.utils.RestTemplateClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.*; + import org.springframework.web.client.HttpClientErrorException; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = CalendlyApplication.class) + +public class UserAvailabilityControllerIntegrationTests { + + @LocalServerPort + private int port; + + + private String userBaseUrl; + private String availabilityBaseUrl; + + @Autowired + private RestTemplateClient restTemplate; + + @BeforeEach + public void setUp() { + availabilityBaseUrl = "http://localhost:" + port + "/api/v1/availability"; + userBaseUrl = "http://localhost:" + port + "/api/v1/user"; + } + + // Test Create User Availability with meaningful method name + @Test + public void createUserAvailability_ShouldReturn201_WhenAvailabilityIsCreatedSuccessfully() { + // create user first + User newUser = new User(null, "Elizabeth", "Swann", "elizabeth.swann@example.com"); + ResponseEntity response = restTemplate.postForEntity(userBaseUrl, newUser, User.class); + String id = response.getBody().getId().toString(); + + Schedule schedule = new Schedule(1696176000000L, 900, 1200, RecurrenceType.WEEKLY, 1696262400000L); + List scheduleList = Arrays.asList(schedule); + Availabilityrequest availabilityRequest = new Availabilityrequest(id, scheduleList); + + ResponseEntity responseEntity = restTemplate.postForEntity(availabilityBaseUrl, availabilityRequest, Void.class); + + assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode()); + } + + // Test Create User Availability with User Not Found + @Test + public void createUserAvailability_ShouldReturn404_WhenUserNotFound() { + Schedule schedule = new Schedule(1696176000000L, 900, 1200, RecurrenceType.WEEKLY, 1696262400000L); + List scheduleList = Arrays.asList(schedule); + Availabilityrequest availabilityRequest = new Availabilityrequest("nonexistent_id", scheduleList); + + try { + restTemplate.postForEntity(availabilityBaseUrl, availabilityRequest, Void.class); + + } catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); + } + } +// +// // Test Get User Availability with meaningful method name + @Test + public void getUserAvailability_ShouldReturn200_WhenUserExists() { + //create user + User newUser = new User(null, "Elizabeth", "Swann", "elizabeth.swann@example.com"); + ResponseEntity response = restTemplate.postForEntity(userBaseUrl, newUser, User.class); + String id = response.getBody().getId().toString(); + + // First create availability for a user + // start date 1704844800000L = 2024/01/10 00:00:00 UTC + //end date 1704931200000L = 2024/01/11 00:00:00 UTC + Schedule schedule = new Schedule(1704844800000L, 3600000, 7200000, RecurrenceType.NONE, 1704931200000L); + List scheduleList = Arrays.asList(schedule); + Availabilityrequest availabilityRequest = new Availabilityrequest(id, scheduleList); + restTemplate.postForEntity(availabilityBaseUrl, availabilityRequest, Void.class); + + // Now retrieve the availability for the user + ResponseEntity responseEntity = restTemplate.getForEntity(availabilityBaseUrl + "/{id}", AvailabilityResponse.class, id); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertEquals(1, responseEntity.getBody().getScheduleList().size()); + assertEquals(3600000, responseEntity.getBody().getScheduleList().get(0).getStartTime()); + assertEquals(RecurrenceType.NONE, responseEntity.getBody().getScheduleList().get(0).getRecurrenceType()); + } +// +// // Test Get User Availability with User Not Found + @Test + public void getUserAvailability_ShouldReturn404_WhenUserNotFound() { + try { + restTemplate.getForEntity(availabilityBaseUrl + "/{id}", AvailabilityResponse.class, "nonexistent_id"); + } catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); + } + } +} diff --git a/src/test/java/ApiIntegrationTests/UserProfileControllerIntegrationTests.java b/src/test/java/ApiIntegrationTests/UserProfileControllerIntegrationTests.java new file mode 100644 index 00000000..8f5d7c84 --- /dev/null +++ b/src/test/java/ApiIntegrationTests/UserProfileControllerIntegrationTests.java @@ -0,0 +1,122 @@ +package ApiIntegrationTests; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.p0.calendly.CalendlyApplication; +import org.p0.calendly.models.User; +import org.p0.calendly.utils.RestTemplateClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.*; +import org.springframework.web.client.HttpClientErrorException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = CalendlyApplication.class) + +public class UserProfileControllerIntegrationTests { + + @LocalServerPort + private int port; + + + private String baseUrl; + + @Autowired + private RestTemplateClient restTemplate; + + @BeforeEach + public void setUp() { + baseUrl = "http://localhost:" + port + "/api/v1/user"; + } + + // Test Create User + @Test + public void createUser_ShouldReturn201_WhenUserIsCreatedSuccessfully() { + User newUser = new User(null, "John", "Doe", "john.doe@example.com"); + + ResponseEntity responseEntity = restTemplate.postForEntity(baseUrl, newUser, User.class); + + assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertNotNull(responseEntity.getBody().getId()); + assertEquals("John", responseEntity.getBody().getFirstName()); + + } + + @Test + public void createUser_ShouldReturn409_WhenUserAlreadyExists() { + // First create a user + User newUser = new User(null, "Elizabeth", "Swann", "elizabeth.swann@example.com"); + ResponseEntity response = restTemplate.postForEntity(baseUrl, newUser, User.class); + String id = response.getBody().getId().toString(); + + // Try to create the same user again + User sameUser = new User(id, "Elizabeth", "Swann", "elizabeth.swann@example.com"); + try { + restTemplate.postForEntity(baseUrl, sameUser, User.class); + } catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.CONFLICT, ex.getStatusCode()); + } + } + + @Test + public void getUserById_ShouldReturn200_WhenUserExists() { + // First create a user + User newUser = new User(null, "Jane", "Doe", "jane.doe@example.com"); + ResponseEntity response = restTemplate.postForEntity(baseUrl, newUser, User.class); + + String id = response.getBody().getId().toString(); + // Now get the user by ID + ResponseEntity responseEntity = restTemplate.getForEntity(baseUrl + "/{id}", User.class, id); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertEquals("Jane", responseEntity.getBody().getFirstName()); + } + + @Test + public void updateUser_ShouldReturn200_WhenUserIsUpdatedSuccessfully() { + // First create a user + User newUser = new User(null, "Jack", "Sparrow", "jack.sparrow@example.com"); + ResponseEntity response = restTemplate.postForEntity(baseUrl, newUser, User.class); + String id = response.getBody().getId().toString(); + + // Update user details + User updatedUser = new User(id, "Captain", "Sparrow", "captain.sparrow@example.com"); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(updatedUser, headers); + + ResponseEntity responseEntity = restTemplate.exchange(baseUrl + "/{id}", HttpMethod.PUT, entity, User.class, id); + + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + assertNotNull(responseEntity.getBody()); + assertEquals("Captain", responseEntity.getBody().getFirstName()); + } + + @Test + public void deleteUser_ShouldReturn204_WhenUserIsDeletedSuccessfully() { + // First create a user + User newUser = new User(null, "Will", "Turner", "will.turner@example.com"); + ResponseEntity response = restTemplate.postForEntity(baseUrl, newUser, User.class); + String id = response.getBody().getId().toString(); + + // Delete the user + restTemplate.delete(baseUrl + "/{id}", id); + + try { + restTemplate.getForEntity(baseUrl + "/{id}", User.class, "3"); + } catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); + } + + } +} \ No newline at end of file diff --git a/src/test/java/TestUtils.java b/src/test/java/TestUtils.java new file mode 100644 index 00000000..bc6dc1bc --- /dev/null +++ b/src/test/java/TestUtils.java @@ -0,0 +1,5 @@ +import org.p0.calendly.models.User; +import org.springframework.http.ResponseEntity; + +public class TestUtils { +} diff --git a/src/test/java/UnitTests/UserServiceTest.java b/src/test/java/UnitTests/UserServiceTest.java new file mode 100644 index 00000000..32bb38f3 --- /dev/null +++ b/src/test/java/UnitTests/UserServiceTest.java @@ -0,0 +1,89 @@ +package org.p0.calendly.services; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.p0.calendly.exceptions.UserAlreadyExistsException; +import org.p0.calendly.exceptions.UserNotFoundException; +import org.p0.calendly.models.User; + +import static org.junit.jupiter.api.Assertions.*; + +class UserServiceTest { + + private UserService userService; + private User testUser; + + @BeforeEach + void setUp() { + userService = new UserService(); + testUser = new User(null, "John", "Doe", "john.doe@example.com"); + } + + @Test + void testCreateUser() { + User createdUser = userService.createUser(testUser); + assertNotNull(createdUser); + assertNotNull(createdUser.getId()); + assertEquals("John", createdUser.getFirstName()); + assertEquals("Doe", createdUser.getLastName()); + assertEquals("john.doe@example.com", createdUser.getEmail()); + } + + @Test + void testCreateUser_ThrowsUserAlreadyExistsException() { + User sameUser = userService.createUser(testUser); + assertThrows(UserAlreadyExistsException.class, () -> { + userService.createUser(sameUser); + }); + } + + @Test + void testGetUserById() { + User user = userService.createUser(testUser); + User fetchedUser = userService.getUserById(user.getId()); + assertNotNull(fetchedUser); + assertEquals("John", fetchedUser.getFirstName()); + } + + @Test + void testGetUserById_ThrowsUserNotFoundException() { + assertThrows(UserNotFoundException.class, () -> { + userService.getUserById("999"); + }); + } + + @Test + void testUpdateUser() { + User user = userService.createUser(testUser); + testUser.setFirstName("Jane"); + testUser.setLastName("Smith"); + testUser.setEmail("jane.smith@example.com"); + + User updatedUser = userService.updateUser(user.getId(), testUser); + + assertNotNull(updatedUser); + assertEquals("Jane", updatedUser.getFirstName()); + assertEquals("Smith", updatedUser.getLastName()); + assertEquals("jane.smith@example.com", updatedUser.getEmail()); + } + + @Test + void testUpdateUser_ThrowsUserNotFoundException() { + testUser.setFirstName("Jane"); + + assertThrows(UserNotFoundException.class, () -> { + userService.updateUser("99", testUser); + }); + } + + @Test + void testDeleteUser() { + User user = userService.createUser(testUser); + String userId = user.getId(); + userService.deleteUser(userId); + + assertThrows(UserNotFoundException.class, () -> { + userService.getUserById(userId); + }); + } +} \ No newline at end of file