From fa290097a5c7304e7711082174246b731278b6e8 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Fri, 23 May 2025 22:44:54 +0300 Subject: [PATCH 1/8] get-daily-task-request & user-secured request sending added --- build.gradle | 1 + .../controller/UserController.java | 99 ++++++++++++++++--- .../com/smartcalendar/dto/DailyTaskDto.java | 24 +++++ .../repository/TaskRepository.java | 3 +- .../smartcalendar/service/UserService.java | 30 +++++- 5 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/smartcalendar/dto/DailyTaskDto.java diff --git a/build.gradle b/build.gradle index 246a245..74cdbc0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' + id 'com.github.jk1.dependency-license-report' version '2.8' } group = 'com.smartcalendar' diff --git a/src/main/java/com/smartcalendar/controller/UserController.java b/src/main/java/com/smartcalendar/controller/UserController.java index c796f82..aade817 100644 --- a/src/main/java/com/smartcalendar/controller/UserController.java +++ b/src/main/java/com/smartcalendar/controller/UserController.java @@ -1,5 +1,6 @@ package com.smartcalendar.controller; +import com.smartcalendar.dto.DailyTaskDto; import com.smartcalendar.model.Event; import com.smartcalendar.model.Task; import com.smartcalendar.model.User; @@ -12,6 +13,7 @@ import java.util.List; import java.util.Map; +import java.util.UUID; @RestController @RequestMapping("/api/users") @@ -32,20 +34,45 @@ public ResponseEntity getUserById(@PathVariable Long id) { } @PutMapping("/{id}/email") - public ResponseEntity updateEmail(@PathVariable Long id, @RequestBody String newEmail) { + public ResponseEntity updateEmail( + @PathVariable Long id, + @RequestBody String newEmail, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(id)) { + return ResponseEntity.status(403).build(); + } User updatedUser = userService.updateEmail(id, newEmail); return ResponseEntity.ok(updatedUser); } @PatchMapping("/tasks/{taskId}/status") - public ResponseEntity updateTaskStatus(@PathVariable Long taskId, @RequestBody Map requestBody) { + public ResponseEntity updateTaskStatus( + @PathVariable UUID taskId, + @RequestBody Map requestBody, + @AuthenticationPrincipal UserDetails userDetails) { + Task task = userService.getTaskById(taskId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!task.getUser().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } boolean completed = requestBody.get("completed"); Task updatedTask = userService.updateTaskStatus(taskId, completed); return ResponseEntity.ok(updatedTask); } @GetMapping("/tasks/{taskId}/description") - public ResponseEntity getTaskDescription(@PathVariable Long taskId) { + public ResponseEntity getTaskDescription( + @PathVariable UUID taskId, + @AuthenticationPrincipal UserDetails userDetails) { + Task task = userService.getTaskById(taskId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!task.getUser().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } String description = userService.getTaskDescription(taskId); return ResponseEntity.ok(description); } @@ -63,35 +90,71 @@ public ResponseEntity createUser(@RequestBody User user) { } @GetMapping("/{userId}/tasks") - public ResponseEntity> getTasksByUserId(@PathVariable Long userId) { + public ResponseEntity> getTasksByUserId( + @PathVariable Long userId, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } List tasks = userService.findTasksByUserId(userId); return ResponseEntity.ok(tasks); } @GetMapping("/{userId}/events") - public ResponseEntity> getEventsByUserId(@PathVariable Long userId) { + public ResponseEntity> getEventsByUserId( + @PathVariable Long userId, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } List events = userService.findEventsByUserId(userId); return ResponseEntity.ok(events); } @PostMapping("/{userId}/events") - public ResponseEntity createEvent(@PathVariable Long userId, @RequestBody Event event) { - User user = userService.findUserById(userId); - event.setOrganizer(user); + public ResponseEntity createEvent( + @PathVariable Long userId, + @RequestBody Event event, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } + event.setOrganizer(currentUser); Event createdEvent = userService.createEvent(event); return ResponseEntity.ok(createdEvent); } @PostMapping("/{userId}/tasks") - public ResponseEntity createTask(@PathVariable Long userId, @RequestBody Task task) { - User user = userService.findUserById(userId); - task.setUser(user); + public ResponseEntity createTask( + @PathVariable Long userId, + @RequestBody Task task, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } + task.setUser(currentUser); Task createdTask = userService.createTask(task); return ResponseEntity.ok(createdTask); } @DeleteMapping("/tasks/{taskId}") - public ResponseEntity deleteTask(@PathVariable Long taskId) { + public ResponseEntity deleteTask( + @PathVariable UUID taskId, + @AuthenticationPrincipal UserDetails userDetails) { + Task task = userService.getTaskById(taskId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!task.getUser().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } userService.deleteTask(taskId); return ResponseEntity.noContent().build(); } @@ -108,4 +171,16 @@ public ResponseEntity> getCurrentUserInfo(@AuthenticationPri return ResponseEntity.ok(result); } + @GetMapping("/{userId}/events/dailytasks") + public ResponseEntity> getAllEventsAsDailyTasks( + @PathVariable Long userId, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } + List dailyTasks = userService.findAllEventsAsDailyTaskDto(userId); + return ResponseEntity.ok(dailyTasks); + } } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/DailyTaskDto.java b/src/main/java/com/smartcalendar/dto/DailyTaskDto.java new file mode 100644 index 0000000..183f43b --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/DailyTaskDto.java @@ -0,0 +1,24 @@ +package com.smartcalendar.dto; + +import com.smartcalendar.model.EventType; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class DailyTaskDto { + private UUID id; + private String title; + private boolean isComplete; + private EventType type; + private LocalDateTime creationTime; + private String description; + private LocalTime start; + private LocalTime end; + private LocalDate date; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/repository/TaskRepository.java b/src/main/java/com/smartcalendar/repository/TaskRepository.java index 7424fb5..9f4eafd 100644 --- a/src/main/java/com/smartcalendar/repository/TaskRepository.java +++ b/src/main/java/com/smartcalendar/repository/TaskRepository.java @@ -5,8 +5,9 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.UUID; @Repository -public interface TaskRepository extends JpaRepository { +public interface TaskRepository extends JpaRepository { List findByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/UserService.java b/src/main/java/com/smartcalendar/service/UserService.java index 0a75db3..d0dc63c 100644 --- a/src/main/java/com/smartcalendar/service/UserService.java +++ b/src/main/java/com/smartcalendar/service/UserService.java @@ -1,12 +1,12 @@ package com.smartcalendar.service; +import com.smartcalendar.dto.DailyTaskDto; import com.smartcalendar.model.Event; import com.smartcalendar.model.Task; import com.smartcalendar.model.User; import com.smartcalendar.repository.EventRepository; import com.smartcalendar.repository.TaskRepository; import com.smartcalendar.repository.UserRepository; -import jakarta.validation.constraints.Email; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -18,6 +18,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -58,7 +60,7 @@ public Task createTask(Task task) { return taskRepository.save(task); } - public void deleteTask(Long taskId) { + public void deleteTask(UUID taskId) { taskRepository.deleteById(taskId); } @@ -117,7 +119,7 @@ public User updateEmail(Long id, String newEmail) { } @Transactional - public Task updateTaskStatus(Long taskId, boolean completed) { + public Task updateTaskStatus(UUID taskId, boolean completed) { Task task = taskRepository.findById(taskId) .orElseThrow(() -> new RuntimeException("Task not found")); task.setCompleted(completed); @@ -125,7 +127,7 @@ public Task updateTaskStatus(Long taskId, boolean completed) { } @Transactional(readOnly = true) - public String getTaskDescription(Long taskId) { + public String getTaskDescription(UUID taskId) { Task task = taskRepository.findById(taskId) .orElseThrow(() -> new RuntimeException("Task not found")); return task.getDescription(); @@ -142,4 +144,24 @@ public Event createEvent(Event event) { public Optional findByUsername(String username) { return userRepository.findByUsername(username); } + + public List findAllEventsAsDailyTaskDto(Long userId) { + List events = findEventsByUserId(userId); + return events.stream().map(event -> new DailyTaskDto( + event.getId(), + event.getTitle(), + false, // isComplete — если нужно, добавьте логику + event.getType(), + event.getCreationTime(), + event.getDescription(), + event.getStart() != null ? event.getStart().toLocalTime() : null, + event.getEnd() != null ? event.getEnd().toLocalTime() : null, + event.getStart() != null ? event.getStart().toLocalDate() : null + )).collect(Collectors.toList()); + } + + public Task getTaskById(UUID taskId) { + return taskRepository.findById(taskId) + .orElseThrow(() -> new RuntimeException("Task not found")); + } } \ No newline at end of file From 61b17f466229e4cb3dadc5699e6f4361df27fe23 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Sun, 25 May 2025 12:38:10 +0300 Subject: [PATCH 2/8] raw-response for event\task creation + event deletion&update requests --- .../controller/UserController.java | 47 +++++++++++++++---- .../smartcalendar/service/UserService.java | 27 ++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/smartcalendar/controller/UserController.java b/src/main/java/com/smartcalendar/controller/UserController.java index aade817..a15276d 100644 --- a/src/main/java/com/smartcalendar/controller/UserController.java +++ b/src/main/java/com/smartcalendar/controller/UserController.java @@ -48,7 +48,7 @@ public ResponseEntity updateEmail( } @PatchMapping("/tasks/{taskId}/status") - public ResponseEntity updateTaskStatus( + public ResponseEntity updateTaskStatus( @PathVariable UUID taskId, @RequestBody Map requestBody, @AuthenticationPrincipal UserDetails userDetails) { @@ -59,8 +59,8 @@ public ResponseEntity updateTaskStatus( return ResponseEntity.status(403).build(); } boolean completed = requestBody.get("completed"); - Task updatedTask = userService.updateTaskStatus(taskId, completed); - return ResponseEntity.ok(updatedTask); + userService.updateTaskStatus(taskId, completed); + return ResponseEntity.ok().build(); } @GetMapping("/tasks/{taskId}/description") @@ -116,7 +116,7 @@ public ResponseEntity> getEventsByUserId( } @PostMapping("/{userId}/events") - public ResponseEntity createEvent( + public ResponseEntity createEvent( @PathVariable Long userId, @RequestBody Event event, @AuthenticationPrincipal UserDetails userDetails) { @@ -126,12 +126,41 @@ public ResponseEntity createEvent( return ResponseEntity.status(403).build(); } event.setOrganizer(currentUser); - Event createdEvent = userService.createEvent(event); - return ResponseEntity.ok(createdEvent); + userService.createEvent(event); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/events/{eventId}") + public ResponseEntity deleteEvent( + @PathVariable UUID eventId, + @AuthenticationPrincipal UserDetails userDetails) { + Event event = userService.getEventById(eventId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!event.getOrganizer().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } + userService.deleteEvent(eventId); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/events/{eventId}") + public ResponseEntity updateEvent( + @PathVariable UUID eventId, + @RequestBody Event event, + @AuthenticationPrincipal UserDetails userDetails) { + Event existingEvent = userService.getEventById(eventId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!existingEvent.getOrganizer().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } + userService.updateEvent(eventId, event); + return ResponseEntity.ok().build(); } @PostMapping("/{userId}/tasks") - public ResponseEntity createTask( + public ResponseEntity createTask( @PathVariable Long userId, @RequestBody Task task, @AuthenticationPrincipal UserDetails userDetails) { @@ -141,8 +170,8 @@ public ResponseEntity createTask( return ResponseEntity.status(403).build(); } task.setUser(currentUser); - Task createdTask = userService.createTask(task); - return ResponseEntity.ok(createdTask); + userService.createTask(task); + return ResponseEntity.ok().build(); } @DeleteMapping("/tasks/{taskId}") diff --git a/src/main/java/com/smartcalendar/service/UserService.java b/src/main/java/com/smartcalendar/service/UserService.java index d0dc63c..8d57d69 100644 --- a/src/main/java/com/smartcalendar/service/UserService.java +++ b/src/main/java/com/smartcalendar/service/UserService.java @@ -60,10 +60,16 @@ public Task createTask(Task task) { return taskRepository.save(task); } + @Transactional public void deleteTask(UUID taskId) { taskRepository.deleteById(taskId); } + @Transactional + public void deleteEvent(UUID eventId) { + eventRepository.deleteById(eventId); + } + public boolean existsByUsername(String username) { return userRepository.existsByUsername(username); } @@ -126,6 +132,20 @@ public Task updateTaskStatus(UUID taskId, boolean completed) { return taskRepository.save(task); } + @Transactional + public void updateEvent(UUID eventId, Event event) { + Event existingEvent = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + existingEvent.setTitle(event.getTitle()); + existingEvent.setDescription(event.getDescription()); + existingEvent.setStart(event.getStart()); + existingEvent.setEnd(event.getEnd()); + existingEvent.setLocation(event.getLocation()); + existingEvent.setType(event.getType()); + existingEvent.setCreationTime(event.getCreationTime()); + eventRepository.save(existingEvent); + } + @Transactional(readOnly = true) public String getTaskDescription(UUID taskId) { Task task = taskRepository.findById(taskId) @@ -150,7 +170,7 @@ public List findAllEventsAsDailyTaskDto(Long userId) { return events.stream().map(event -> new DailyTaskDto( event.getId(), event.getTitle(), - false, // isComplete — если нужно, добавьте логику + false, event.getType(), event.getCreationTime(), event.getDescription(), @@ -164,4 +184,9 @@ public Task getTaskById(UUID taskId) { return taskRepository.findById(taskId) .orElseThrow(() -> new RuntimeException("Task not found")); } + + public Event getEventById(UUID eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + } } \ No newline at end of file From 852d65b6eb889ad43debbf77d3453e9fed547073 Mon Sep 17 00:00:00 2001 From: Dmitriy Rusanov Date: Sun, 1 Jun 2025 15:25:38 +0300 Subject: [PATCH 3/8] statistics api improvement & license & minor event request additions --- .github/workflows/ci.yml | 11 +- build.gradle | 2 +- .../controller/StatisticsController.java | 31 ++-- .../controller/UserController.java | 79 +++++++--- .../smartcalendar/dto/AverageDayTimeDto.java | 13 ++ .../smartcalendar/dto/AverageDayTimeVars.java | 10 -- .../dto/ContinuesSuccessDaysDto.java | 13 ++ .../com/smartcalendar/dto/StatisticsData.java | 16 +++ .../com/smartcalendar/dto/TodayTimeDto.java | 13 ++ ...kTypes.java => TotalTimeTaskTypesDto.java} | 8 +- .../java/com/smartcalendar/model/Event.java | 4 +- .../com/smartcalendar/model/Statistics.java | 36 +++++ .../repository/StatisticsRepository.java | 12 ++ .../service/StatisticsService.java | 136 +++++++++++++++--- .../smartcalendar/service/UserService.java | 30 +++- 15 files changed, 353 insertions(+), 61 deletions(-) create mode 100644 src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java delete mode 100644 src/main/java/com/smartcalendar/dto/AverageDayTimeVars.java create mode 100644 src/main/java/com/smartcalendar/dto/ContinuesSuccessDaysDto.java create mode 100644 src/main/java/com/smartcalendar/dto/StatisticsData.java create mode 100644 src/main/java/com/smartcalendar/dto/TodayTimeDto.java rename src/main/java/com/smartcalendar/dto/{TotalTimeTaskTypes.java => TotalTimeTaskTypesDto.java} (70%) create mode 100644 src/main/java/com/smartcalendar/model/Statistics.java create mode 100644 src/main/java/com/smartcalendar/repository/StatisticsRepository.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5aa3b..def0487 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,8 +36,17 @@ jobs: - name: Build and test run: ./gradlew clean build + - name: Generate license report + run: ./gradlew generateLicenseReport + - name: Upload test results uses: actions/upload-artifact@v4 with: name: test-results - path: build/test-results/test \ No newline at end of file + path: build/test-results/test + + - name: Upload license report + uses: actions/upload-artifact@v4 + with: + name: license-report + path: build/reports/dependency-license \ No newline at end of file diff --git a/build.gradle b/build.gradle index 74cdbc0..38e3807 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'com.smartcalendar' -version = '0.0.1-SNAPSHOT' +version = '0.0.3-SNAPSHOT' java { toolchain { diff --git a/src/main/java/com/smartcalendar/controller/StatisticsController.java b/src/main/java/com/smartcalendar/controller/StatisticsController.java index b293c17..2d10506 100644 --- a/src/main/java/com/smartcalendar/controller/StatisticsController.java +++ b/src/main/java/com/smartcalendar/controller/StatisticsController.java @@ -1,9 +1,13 @@ package com.smartcalendar.controller; import com.smartcalendar.dto.*; +import com.smartcalendar.model.User; import com.smartcalendar.service.StatisticsService; +import com.smartcalendar.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,24 +18,35 @@ public class StatisticsController { private final StatisticsService statisticsService; + private final UserService userService; + + private Long getCurrentUserId(UserDetails userDetails) { + User user = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + return user.getId(); + } @GetMapping("/total-time-task-types") - public ResponseEntity getTotalTimeTaskTypes() { - return ResponseEntity.ok(statisticsService.getTotalTimeTaskTypes()); + public ResponseEntity getTotalTimeTaskTypes(@AuthenticationPrincipal UserDetails userDetails) { + Long userId = getCurrentUserId(userDetails); + return ResponseEntity.ok(statisticsService.getTotalTimeTaskTypes(userId)); } @GetMapping("/today") - public ResponseEntity getTodayTimeVars() { - return ResponseEntity.ok(statisticsService.getTodayTimeVars()); + public ResponseEntity getTodayTimeDto(@AuthenticationPrincipal UserDetails userDetails) { + Long userId = getCurrentUserId(userDetails); + return ResponseEntity.ok(statisticsService.getTodayTimeDto(userId)); } @GetMapping("/continuous-success-days") - public ResponseEntity getContinuousSuccessDaysVars() { - return ResponseEntity.ok(statisticsService.getContinuousSuccessDaysVars()); + public ResponseEntity getContinuesSuccessDaysDto(@AuthenticationPrincipal UserDetails userDetails) { + Long userId = getCurrentUserId(userDetails); + return ResponseEntity.ok(statisticsService.getContinuesSuccessDaysDto(userId)); } @GetMapping("/average-day-time") - public ResponseEntity getAverageDayTimeVars() { - return ResponseEntity.ok(statisticsService.getAverageDayTimeVars()); + public ResponseEntity getAverageDayTimeDto(@AuthenticationPrincipal UserDetails userDetails) { + Long userId = getCurrentUserId(userDetails); + return ResponseEntity.ok(statisticsService.getAverageDayTimeDto(userId)); } } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/controller/UserController.java b/src/main/java/com/smartcalendar/controller/UserController.java index a15276d..425b81a 100644 --- a/src/main/java/com/smartcalendar/controller/UserController.java +++ b/src/main/java/com/smartcalendar/controller/UserController.java @@ -1,6 +1,7 @@ package com.smartcalendar.controller; import com.smartcalendar.dto.DailyTaskDto; +import com.smartcalendar.dto.StatisticsData; import com.smartcalendar.model.Event; import com.smartcalendar.model.Task; import com.smartcalendar.model.User; @@ -116,7 +117,7 @@ public ResponseEntity> getEventsByUserId( } @PostMapping("/{userId}/events") - public ResponseEntity createEvent( + public ResponseEntity> createEvent( @PathVariable Long userId, @RequestBody Event event, @AuthenticationPrincipal UserDetails userDetails) { @@ -126,22 +127,9 @@ public ResponseEntity createEvent( return ResponseEntity.status(403).build(); } event.setOrganizer(currentUser); - userService.createEvent(event); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/events/{eventId}") - public ResponseEntity deleteEvent( - @PathVariable UUID eventId, - @AuthenticationPrincipal UserDetails userDetails) { - Event event = userService.getEventById(eventId); - User currentUser = userService.findByUsername(userDetails.getUsername()) - .orElseThrow(() -> new RuntimeException("User not found")); - if (!event.getOrganizer().getId().equals(currentUser.getId())) { - return ResponseEntity.status(403).build(); - } - userService.deleteEvent(eventId); - return ResponseEntity.noContent().build(); + event.setId(null); + Event createdEvent = userService.createEvent(event); + return ResponseEntity.ok(Map.of("id", createdEvent.getId())); } @PatchMapping("/events/{eventId}") @@ -212,4 +200,61 @@ public ResponseEntity> getAllEventsAsDailyTasks( List dailyTasks = userService.findAllEventsAsDailyTaskDto(userId); return ResponseEntity.ok(dailyTasks); } + + @PatchMapping("/events/{eventId}/status") + public ResponseEntity updateEventStatus( + @PathVariable UUID eventId, + @RequestBody Map requestBody, + @AuthenticationPrincipal UserDetails userDetails) { + Event event = userService.getEventById(eventId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!event.getOrganizer().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } + boolean completed = requestBody.get("completed"); + Event updatedEvent = userService.updateEventStatus(eventId, completed); + return ResponseEntity.ok(updatedEvent); + } + + @DeleteMapping("/events/{eventId}") + public ResponseEntity> deleteEventById( + @PathVariable UUID eventId, + @AuthenticationPrincipal UserDetails userDetails) { + Event event = userService.getEventById(eventId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!event.getOrganizer().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } + UUID deletedId = userService.deleteEventById(eventId); + return ResponseEntity.ok(Map.of("id", deletedId)); + } + + @GetMapping("/{userId}/statistics") + public ResponseEntity getStatistics( + @PathVariable Long userId, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } + StatisticsData statistics = userService.getStatistics(userId); + return ResponseEntity.ok(statistics); + } + + @PutMapping("/{userId}/statistics") + public ResponseEntity updateStatistics( + @PathVariable Long userId, + @RequestBody StatisticsData statisticsData, + @AuthenticationPrincipal UserDetails userDetails) { + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!currentUser.getId().equals(userId)) { + return ResponseEntity.status(403).build(); + } + userService.updateStatistics(userId, statisticsData); + return ResponseEntity.ok().build(); + } } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java b/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java new file mode 100644 index 0000000..689a151 --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java @@ -0,0 +1,13 @@ +package com.smartcalendar.dto; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AverageDayTimeDto { + private long totalWorkMinutes; + private long totalDays; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/AverageDayTimeVars.java b/src/main/java/com/smartcalendar/dto/AverageDayTimeVars.java deleted file mode 100644 index bc5df5f..0000000 --- a/src/main/java/com/smartcalendar/dto/AverageDayTimeVars.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.smartcalendar.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class AverageDayTimeVars { - private long averageMinutesPerDay; -} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/ContinuesSuccessDaysDto.java b/src/main/java/com/smartcalendar/dto/ContinuesSuccessDaysDto.java new file mode 100644 index 0000000..9e0188f --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/ContinuesSuccessDaysDto.java @@ -0,0 +1,13 @@ +package com.smartcalendar.dto; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ContinuesSuccessDaysDto { + private int record; + private int now; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/StatisticsData.java b/src/main/java/com/smartcalendar/dto/StatisticsData.java new file mode 100644 index 0000000..79aaf25 --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/StatisticsData.java @@ -0,0 +1,16 @@ +package com.smartcalendar.dto; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class StatisticsData { + private TotalTimeTaskTypesDto totalTime = new TotalTimeTaskTypesDto(0, 0, 0, 0); + private long weekTime = 0; + private TodayTimeDto todayTime = new TodayTimeDto(0, 0); + private ContinuesSuccessDaysDto continuesSuccessDays = new ContinuesSuccessDaysDto(0, 0); + private AverageDayTimeDto averageDayTime = new AverageDayTimeDto(0, 0); +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/TodayTimeDto.java b/src/main/java/com/smartcalendar/dto/TodayTimeDto.java new file mode 100644 index 0000000..88d3f15 --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/TodayTimeDto.java @@ -0,0 +1,13 @@ +package com.smartcalendar.dto; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TodayTimeDto { + private long planned; + private long completed; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/TotalTimeTaskTypes.java b/src/main/java/com/smartcalendar/dto/TotalTimeTaskTypesDto.java similarity index 70% rename from src/main/java/com/smartcalendar/dto/TotalTimeTaskTypes.java rename to src/main/java/com/smartcalendar/dto/TotalTimeTaskTypesDto.java index 46fd111..9493d19 100644 --- a/src/main/java/com/smartcalendar/dto/TotalTimeTaskTypes.java +++ b/src/main/java/com/smartcalendar/dto/TotalTimeTaskTypesDto.java @@ -1,13 +1,15 @@ package com.smartcalendar.dto; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor @AllArgsConstructor -public class TotalTimeTaskTypes { +public class TotalTimeTaskTypesDto { private long common; private long work; private long study; private long fitness; -} +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/model/Event.java b/src/main/java/com/smartcalendar/model/Event.java index cd68fc9..fe3ba25 100644 --- a/src/main/java/com/smartcalendar/model/Event.java +++ b/src/main/java/com/smartcalendar/model/Event.java @@ -31,11 +31,13 @@ public class Event { private String location; @Enumerated(EnumType.STRING) - private EventType type; // Аналог DailyTaskType + private EventType type; private LocalDateTime creationTime = LocalDateTime.now(); @ManyToOne @JoinColumn(name = "organizer_id") private User organizer; + + private boolean completed = false; } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/model/Statistics.java b/src/main/java/com/smartcalendar/model/Statistics.java new file mode 100644 index 0000000..867fad7 --- /dev/null +++ b/src/main/java/com/smartcalendar/model/Statistics.java @@ -0,0 +1,36 @@ +package com.smartcalendar.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "statistics") +@Data +@NoArgsConstructor +public class Statistics { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "user_id", unique = true) + private User user; + + private long totalCommon; + private long totalWork; + private long totalStudy; + private long totalFitness; + + private long weekTime; + + private long todayPlanned; + private long todayCompleted; + + private int continuesRecord; + private int continuesNow; + + private long averageWorkMinutes; + private long averageTotalDays; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/repository/StatisticsRepository.java b/src/main/java/com/smartcalendar/repository/StatisticsRepository.java new file mode 100644 index 0000000..d325880 --- /dev/null +++ b/src/main/java/com/smartcalendar/repository/StatisticsRepository.java @@ -0,0 +1,12 @@ +package com.smartcalendar.repository; + +import com.smartcalendar.model.Statistics; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface StatisticsRepository extends JpaRepository { + Optional findByUserId(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/StatisticsService.java b/src/main/java/com/smartcalendar/service/StatisticsService.java index 669dc89..d72f77f 100644 --- a/src/main/java/com/smartcalendar/service/StatisticsService.java +++ b/src/main/java/com/smartcalendar/service/StatisticsService.java @@ -1,34 +1,134 @@ package com.smartcalendar.service; import com.smartcalendar.dto.*; +import com.smartcalendar.model.Statistics; +import com.smartcalendar.model.User; +import com.smartcalendar.repository.StatisticsRepository; +import com.smartcalendar.repository.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class StatisticsService { - public TotalTimeTaskTypes getTotalTimeTaskTypes() { - long common = 120; - long work = 300; - long study = 180; - long fitness = 60; - return new TotalTimeTaskTypes(common, work, study, fitness); + private final StatisticsRepository statisticsRepository; + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public StatisticsData getStatistics(Long userId) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElse(null); + + if (stats == null) { + return new StatisticsData( + new TotalTimeTaskTypesDto(0, 0, 0, 0), + 0L, + new TodayTimeDto(0, 0), + new ContinuesSuccessDaysDto(0, 0), + new AverageDayTimeDto(0, 0) + ); + } + + return new StatisticsData( + new TotalTimeTaskTypesDto(stats.getTotalCommon(), stats.getTotalWork(), stats.getTotalStudy(), stats.getTotalFitness()), + stats.getWeekTime(), + new TodayTimeDto(stats.getTodayPlanned(), stats.getTodayCompleted()), + new ContinuesSuccessDaysDto(stats.getContinuesRecord(), stats.getContinuesNow()), + new AverageDayTimeDto(stats.getAverageWorkMinutes(), stats.getAverageTotalDays()) + ); } - public TodayTimeVars getTodayTimeVars() { - long planned = 480; - long completed = 300; - return new TodayTimeVars(planned, completed); + @Transactional + public void updateStatistics(Long userId, StatisticsData statisticsData) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElseGet(() -> { + Statistics s = new Statistics(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + s.setUser(user); + return s; + }); + + stats.setTotalCommon(statisticsData.getTotalTime().getCommon()); + stats.setTotalWork(statisticsData.getTotalTime().getWork()); + stats.setTotalStudy(statisticsData.getTotalTime().getStudy()); + stats.setTotalFitness(statisticsData.getTotalTime().getFitness()); + + stats.setWeekTime(statisticsData.getWeekTime()); + + stats.setTodayPlanned(statisticsData.getTodayTime().getPlanned()); + stats.setTodayCompleted(statisticsData.getTodayTime().getCompleted()); + + stats.setContinuesRecord(statisticsData.getContinuesSuccessDays().getRecord()); + stats.setContinuesNow(statisticsData.getContinuesSuccessDays().getNow()); + + stats.setAverageWorkMinutes(statisticsData.getAverageDayTime().getTotalWorkMinutes()); + stats.setAverageTotalDays(statisticsData.getAverageDayTime().getTotalDays()); + + statisticsRepository.save(stats); } - public ContinuousSuccessDaysVars getContinuousSuccessDaysVars() { - int record = 10; - int now = 5; - return new ContinuousSuccessDaysVars(record, now); + @Transactional(readOnly = true) + public TotalTimeTaskTypesDto getTotalTimeTaskTypes(Long userId) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElse(null); + + if (stats == null) { + return new TotalTimeTaskTypesDto(0, 0, 0, 0); + } + + return new TotalTimeTaskTypesDto( + stats.getTotalCommon(), + stats.getTotalWork(), + stats.getTotalStudy(), + stats.getTotalFitness() + ); } - public AverageDayTimeVars getAverageDayTimeVars() { - long totalWorkMinutes = 14400; - long totalDays = 30; - return new AverageDayTimeVars(totalWorkMinutes / totalDays); + @Transactional(readOnly = true) + public TodayTimeDto getTodayTimeDto(Long userId) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElse(null); + + if (stats == null) { + return new TodayTimeDto(0, 0); + } + + return new TodayTimeDto( + stats.getTodayPlanned(), + stats.getTodayCompleted() + ); + } + + @Transactional(readOnly = true) + public ContinuesSuccessDaysDto getContinuesSuccessDaysDto(Long userId) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElse(null); + + if (stats == null) { + return new ContinuesSuccessDaysDto(0, 0); + } + + return new ContinuesSuccessDaysDto( + stats.getContinuesRecord(), + stats.getContinuesNow() + ); + } + + @Transactional(readOnly = true) + public AverageDayTimeDto getAverageDayTimeDto(Long userId) { + Statistics stats = statisticsRepository.findByUserId(userId) + .orElse(null); + + if (stats == null) { + return new AverageDayTimeDto(0, 0); + } + + return new AverageDayTimeDto( + stats.getAverageWorkMinutes(), + stats.getAverageTotalDays() + ); } } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/UserService.java b/src/main/java/com/smartcalendar/service/UserService.java index 8d57d69..88aa21f 100644 --- a/src/main/java/com/smartcalendar/service/UserService.java +++ b/src/main/java/com/smartcalendar/service/UserService.java @@ -1,6 +1,7 @@ package com.smartcalendar.service; import com.smartcalendar.dto.DailyTaskDto; +import com.smartcalendar.dto.StatisticsData; import com.smartcalendar.model.Event; import com.smartcalendar.model.Task; import com.smartcalendar.model.User; @@ -28,6 +29,7 @@ public class UserService { private final TaskRepository taskRepository; private final EventRepository eventRepository; private final PasswordEncoder passwordEncoder; + private final StatisticsService statisticsService; public User createUser(User user) { if (userRepository.existsByUsername(user.getUsername())) { @@ -82,7 +84,7 @@ public boolean existsByEmail(String email) { public boolean changeCredentials(String currentUsername, String currentPassword, String newUsername, String newPassword) { User user = userRepository.findByUsername(currentUsername) .orElseThrow(() -> new RuntimeException("User not found")); - + if (passwordEncoder.matches(currentPassword, user.getPassword())) { if (newUsername != null && !newUsername.isEmpty()) { user.setUsername(newUsername); @@ -158,6 +160,7 @@ public List findEventsByUserId(Long userId) { } public Event createEvent(Event event) { + event.setId(null); return eventRepository.save(event); } @@ -170,7 +173,7 @@ public List findAllEventsAsDailyTaskDto(Long userId) { return events.stream().map(event -> new DailyTaskDto( event.getId(), event.getTitle(), - false, + event.isCompleted(), event.getType(), event.getCreationTime(), event.getDescription(), @@ -189,4 +192,27 @@ public Event getEventById(UUID eventId) { return eventRepository.findById(eventId) .orElseThrow(() -> new RuntimeException("Event not found")); } + + @Transactional + public Event updateEventStatus(UUID eventId, boolean completed) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + event.setCompleted(completed); + return eventRepository.save(event); + } + + @Transactional + public UUID deleteEventById(UUID eventId) { + eventRepository.deleteById(eventId); + return eventId; + } + + public StatisticsData getStatistics(Long userId) { + return statisticsService.getStatistics(userId); + } + + @Transactional + public void updateStatistics(Long userId, StatisticsData statisticsData) { + statisticsService.updateStatistics(userId, statisticsData); + } } \ No newline at end of file From 21aa13b3279fda84c5651d5d0c2fc7cc3a902384 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Wed, 4 Jun 2025 12:38:56 +0300 Subject: [PATCH 4/8] creation&edition changes --- .../controller/UserController.java | 33 +++++++++++--- .../java/com/smartcalendar/model/Event.java | 2 +- .../service/StatisticsService.java | 2 +- .../smartcalendar/service/UserService.java | 44 +++++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/smartcalendar/controller/UserController.java b/src/main/java/com/smartcalendar/controller/UserController.java index 425b81a..0b014d5 100644 --- a/src/main/java/com/smartcalendar/controller/UserController.java +++ b/src/main/java/com/smartcalendar/controller/UserController.java @@ -127,9 +127,12 @@ public ResponseEntity> createEvent( return ResponseEntity.status(403).build(); } event.setOrganizer(currentUser); - event.setId(null); - Event createdEvent = userService.createEvent(event); - return ResponseEntity.ok(Map.of("id", createdEvent.getId())); + try { + Event createdEvent = userService.createEventWithCustomId(event); + return ResponseEntity.ok(Map.of("id", createdEvent.getId())); + } catch (IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); + } } @PatchMapping("/events/{eventId}") @@ -143,12 +146,13 @@ public ResponseEntity updateEvent( if (!existingEvent.getOrganizer().getId().equals(currentUser.getId())) { return ResponseEntity.status(403).build(); } - userService.updateEvent(eventId, event); + userService.editEvent(eventId, event); return ResponseEntity.ok().build(); } + @PostMapping("/{userId}/tasks") - public ResponseEntity createTask( + public ResponseEntity> createTask( @PathVariable Long userId, @RequestBody Task task, @AuthenticationPrincipal UserDetails userDetails) { @@ -158,8 +162,8 @@ public ResponseEntity createTask( return ResponseEntity.status(403).build(); } task.setUser(currentUser); - userService.createTask(task); - return ResponseEntity.ok().build(); + Task createdTask = userService.createTaskWithCustomId(task); + return ResponseEntity.ok(Map.of("id", createdTask.getId())); } @DeleteMapping("/tasks/{taskId}") @@ -176,6 +180,21 @@ public ResponseEntity deleteTask( return ResponseEntity.noContent().build(); } + @PatchMapping("/tasks/{taskId}") + public ResponseEntity editTask( + @PathVariable UUID taskId, + @RequestBody Task task, + @AuthenticationPrincipal UserDetails userDetails) { + Task existingTask = userService.getTaskById(taskId); + User currentUser = userService.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + if (!existingTask.getUser().getId().equals(currentUser.getId())) { + return ResponseEntity.status(403).build(); + } + userService.editTask(taskId, task); + return ResponseEntity.ok().build(); + } + @GetMapping("/me") public ResponseEntity> getCurrentUserInfo(@AuthenticationPrincipal UserDetails userDetails) { User user = userService.findByUsername(userDetails.getUsername()) diff --git a/src/main/java/com/smartcalendar/model/Event.java b/src/main/java/com/smartcalendar/model/Event.java index fe3ba25..bee786a 100644 --- a/src/main/java/com/smartcalendar/model/Event.java +++ b/src/main/java/com/smartcalendar/model/Event.java @@ -15,7 +15,7 @@ @AllArgsConstructor public class Event { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + //@GeneratedValue(strategy = GenerationType.AUTO) private UUID id; private String title; diff --git a/src/main/java/com/smartcalendar/service/StatisticsService.java b/src/main/java/com/smartcalendar/service/StatisticsService.java index d72f77f..4f01e3d 100644 --- a/src/main/java/com/smartcalendar/service/StatisticsService.java +++ b/src/main/java/com/smartcalendar/service/StatisticsService.java @@ -27,7 +27,7 @@ public StatisticsData getStatistics(Long userId) { 0L, new TodayTimeDto(0, 0), new ContinuesSuccessDaysDto(0, 0), - new AverageDayTimeDto(0, 0) + new AverageDayTimeDto(0, 1) ); } diff --git a/src/main/java/com/smartcalendar/service/UserService.java b/src/main/java/com/smartcalendar/service/UserService.java index 88aa21f..84f3100 100644 --- a/src/main/java/com/smartcalendar/service/UserService.java +++ b/src/main/java/com/smartcalendar/service/UserService.java @@ -215,4 +215,48 @@ public StatisticsData getStatistics(Long userId) { public void updateStatistics(Long userId, StatisticsData statisticsData) { statisticsService.updateStatistics(userId, statisticsData); } + + @Transactional + public Task createTaskWithCustomId(Task task) { + if (task.getId() != null && taskRepository.existsById(task.getId())) { + throw new IllegalArgumentException("Task with this id already exists"); + } + return taskRepository.save(task); + } + + @Transactional + public void editTask(UUID taskId, Task task) { + Task existingTask = taskRepository.findById(taskId) + .orElseThrow(() -> new RuntimeException("Task not found")); + existingTask.setTitle(task.getTitle()); + existingTask.setDescription(task.getDescription()); + existingTask.setCompleted(task.isCompleted()); + existingTask.setDueDateTime(task.getDueDateTime()); + existingTask.setAllDay(task.getAllDay()); + existingTask.setCreationTime(task.getCreationTime()); + taskRepository.save(existingTask); + } + + @Transactional + public Event createEventWithCustomId(Event event) { + if (event.getId() != null && eventRepository.existsById(event.getId())) { + throw new IllegalArgumentException("Event with this id already exists"); + } + return eventRepository.save(event); + } + + @Transactional + public void editEvent(UUID eventId, Event event) { + Event existingEvent = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("Event not found")); + existingEvent.setTitle(event.getTitle()); + existingEvent.setDescription(event.getDescription()); + existingEvent.setStart(event.getStart()); + existingEvent.setEnd(event.getEnd()); + existingEvent.setLocation(event.getLocation()); + existingEvent.setType(event.getType()); + existingEvent.setCreationTime(event.getCreationTime()); + existingEvent.setCompleted(event.isCompleted()); + eventRepository.save(existingEvent); + } } \ No newline at end of file From eb494d349a525ca8fc430af62a8fae213e778a26 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Sat, 7 Jun 2025 16:44:01 +0300 Subject: [PATCH 5/8] date saving when registered & some test --- .../smartcalendar/config/SecurityConfig.java | 2 + .../controller/AuthController.java | 58 +++--- .../smartcalendar/dto/AverageDayTimeDto.java | 4 +- .../dto/RegistrationRequest.java | 11 ++ .../com/smartcalendar/dto/StatisticsData.java | 2 +- .../com/smartcalendar/model/Statistics.java | 4 +- .../service/StatisticsService.java | 10 +- .../smartcalendar/service/UserService.java | 17 ++ .../config/TestSecurityConfig.java | 47 +++++ .../AudioControllerIntegrationTest.java | 80 +++++++++ .../controller/AuthControllerTest.java | 48 +++-- .../ChatGPTControllerIntegrationTest.java | 80 +++++++++ .../StatisticsControllerIntegrationTest.java | 104 +++++++++++ .../UserControllerIntegrationTest.java | 90 ++++++++++ .../service/ChatGPTServiceTest.java | 53 ++++++ .../service/StatisticsServiceTest.java | 168 ++++++++++++++++++ .../service/UserServiceTest.java | 71 +++++++- 17 files changed, 795 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/smartcalendar/dto/RegistrationRequest.java create mode 100644 src/test/java/com/smartcalendar/config/TestSecurityConfig.java create mode 100644 src/test/java/com/smartcalendar/controller/AudioControllerIntegrationTest.java create mode 100644 src/test/java/com/smartcalendar/controller/ChatGPTControllerIntegrationTest.java create mode 100644 src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java create mode 100644 src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java create mode 100644 src/test/java/com/smartcalendar/service/ChatGPTServiceTest.java create mode 100644 src/test/java/com/smartcalendar/service/StatisticsServiceTest.java diff --git a/src/main/java/com/smartcalendar/config/SecurityConfig.java b/src/main/java/com/smartcalendar/config/SecurityConfig.java index e924fe2..212f19f 100644 --- a/src/main/java/com/smartcalendar/config/SecurityConfig.java +++ b/src/main/java/com/smartcalendar/config/SecurityConfig.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -25,6 +26,7 @@ @Configuration @EnableWebSecurity @RequiredArgsConstructor +@Profile("!test") public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final UserService userService; diff --git a/src/main/java/com/smartcalendar/controller/AuthController.java b/src/main/java/com/smartcalendar/controller/AuthController.java index 50306ba..a4fcf7a 100644 --- a/src/main/java/com/smartcalendar/controller/AuthController.java +++ b/src/main/java/com/smartcalendar/controller/AuthController.java @@ -1,9 +1,13 @@ package com.smartcalendar.controller; +import com.smartcalendar.dto.RegistrationRequest; import com.smartcalendar.model.User; import com.smartcalendar.service.JwtService; +import com.smartcalendar.service.StatisticsService; import com.smartcalendar.service.UserService; import com.smartcalendar.dto.ChangeCredentialsRequest; +import com.smartcalendar.dto.StatisticsData; +import com.smartcalendar.dto.AverageDayTimeDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -21,6 +25,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; + @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor @@ -30,6 +36,7 @@ public class AuthController { private final AuthenticationManager authenticationManager; private final JwtService jwtService; private final UserService userService; + private final StatisticsService statisticsService; @Operation( summary = "User authentication", @@ -44,16 +51,16 @@ public class AuthController { @PostMapping("/login") public ResponseEntity authenticateUser(@RequestBody User user) { logger.info("Attempting to authenticate user: {}", user.getUsername()); - + if ((user.getUsername() == null && user.getEmail() == null) || user.getPassword() == null) { logger.warn("Username/email or password is null. Username: {}, Email: {}", user.getUsername(), user.getEmail()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Username/email and password are required"); } - + try { logger.debug("Determining if login is by username or email: {}", user.getUsername() != null ? user.getUsername() : user.getEmail()); UserDetails userDetails; - + try { if (user.getUsername() != null) { userDetails = userService.loadUserByUsername(user.getUsername()); @@ -64,17 +71,17 @@ public ResponseEntity authenticateUser(@RequestBody User user) { logger.error("User not found: {}", user.getUsername() != null ? user.getUsername() : user.getEmail()); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid username or email"); } - + Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(userDetails.getUsername(), user.getPassword()) + new UsernamePasswordAuthenticationToken(userDetails.getUsername(), user.getPassword()) ); - + logger.debug("Authentication successful for user: {}", userDetails.getUsername()); SecurityContextHolder.getContext().setAuthentication(authentication); - + String jwt = jwtService.generateToken(userDetails.getUsername()); logger.info("JWT token generated for user: {}", userDetails.getUsername()); - + return ResponseEntity.ok(jwt); } catch (BadCredentialsException e) { logger.error("Invalid credentials for user: {}", user.getUsername() != null ? user.getUsername() : user.getEmail()); @@ -83,7 +90,7 @@ public ResponseEntity authenticateUser(@RequestBody User user) { logger.error("Unexpected error during authentication for user: {}", user.getUsername() != null ? user.getUsername() : user.getEmail(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred"); } -} + } @Operation( summary = "New user signup", @@ -95,29 +102,40 @@ public ResponseEntity authenticateUser(@RequestBody User user) { @ApiResponse(responseCode = "500", description = "Internal server error") }) @PostMapping("/signup") - public ResponseEntity registerUser(@RequestBody User user) { - logger.info("Attempting to register user: {}", user.getUsername()); + public ResponseEntity registerUser(@RequestBody RegistrationRequest request) { + logger.info("Attempting to register user: {}", request.getUsername()); - if (user.getUsername() == null || user.getPassword() == null || user.getEmail() == null) { - logger.warn("Missing required fields for registration. Username: {}, Email: {}", user.getUsername(), user.getEmail()); + if (request.getUsername() == null || request.getPassword() == null || request.getEmail() == null || request.getFirstDay() == null) { + logger.warn("Missing required fields for registration. Username: {}, Email: {}, FirstDay: {}", request.getUsername(), request.getEmail(), request.getFirstDay()); return ResponseEntity.badRequest().body(null); } - if (userService.existsByUsername(user.getUsername())) { - logger.warn("Username already exists: {}", user.getUsername()); + if (userService.existsByUsername(request.getUsername())) { + logger.warn("Username already exists: {}", request.getUsername()); return ResponseEntity.badRequest().body(null); } - if (userService.existsByEmail(user.getEmail())) { - logger.warn("Email already exists: {}", user.getEmail()); + if (userService.existsByEmail(request.getEmail())) { + logger.warn("Email already exists: {}", request.getEmail()); return ResponseEntity.badRequest().body(null); } try { + User user = new User(); + user.setUsername(request.getUsername()); + user.setEmail(request.getEmail()); + user.setPassword(request.getPassword()); User createdUser = userService.createUser(user); - logger.info("User registered successfully: {}", user.getUsername()); + + StatisticsData statisticsData = new StatisticsData(); + statisticsData.setAverageDayTime( + new AverageDayTimeDto(0, LocalDate.parse(request.getFirstDay())) + ); + statisticsService.updateStatistics(createdUser.getId(), statisticsData); + + logger.info("User registered successfully: {}", request.getUsername()); return ResponseEntity.ok(createdUser); } catch (Exception e) { - logger.error("Error during user registration: {}", user.getUsername(), e); + logger.error("Error during user registration: {}", request.getUsername(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); } } @@ -130,7 +148,7 @@ public ResponseEntity changeCredentials(@RequestBody ChangeCredentialsRequest request.getNewUsername(), request.getNewPassword() ); - + if (success) { return ResponseEntity.ok("Credentials updated successfully"); } diff --git a/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java b/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java index 689a151..aa0f861 100644 --- a/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java +++ b/src/main/java/com/smartcalendar/dto/AverageDayTimeDto.java @@ -4,10 +4,12 @@ import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; +import java.time.LocalDate; + @Data @NoArgsConstructor @AllArgsConstructor public class AverageDayTimeDto { private long totalWorkMinutes; - private long totalDays; + private LocalDate firstDay; } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/RegistrationRequest.java b/src/main/java/com/smartcalendar/dto/RegistrationRequest.java new file mode 100644 index 0000000..381ce76 --- /dev/null +++ b/src/main/java/com/smartcalendar/dto/RegistrationRequest.java @@ -0,0 +1,11 @@ +package com.smartcalendar.dto; + +import lombok.Data; + +@Data +public class RegistrationRequest { + private String username; + private String email; + private String password; + private String firstDay; +} \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/dto/StatisticsData.java b/src/main/java/com/smartcalendar/dto/StatisticsData.java index 79aaf25..c370a85 100644 --- a/src/main/java/com/smartcalendar/dto/StatisticsData.java +++ b/src/main/java/com/smartcalendar/dto/StatisticsData.java @@ -12,5 +12,5 @@ public class StatisticsData { private long weekTime = 0; private TodayTimeDto todayTime = new TodayTimeDto(0, 0); private ContinuesSuccessDaysDto continuesSuccessDays = new ContinuesSuccessDaysDto(0, 0); - private AverageDayTimeDto averageDayTime = new AverageDayTimeDto(0, 0); + private AverageDayTimeDto averageDayTime = new AverageDayTimeDto(0, null); } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/model/Statistics.java b/src/main/java/com/smartcalendar/model/Statistics.java index 867fad7..d2dd986 100644 --- a/src/main/java/com/smartcalendar/model/Statistics.java +++ b/src/main/java/com/smartcalendar/model/Statistics.java @@ -4,6 +4,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; + @Entity @Table(name = "statistics") @Data @@ -32,5 +34,5 @@ public class Statistics { private int continuesNow; private long averageWorkMinutes; - private long averageTotalDays; + private LocalDate firstDay; } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/StatisticsService.java b/src/main/java/com/smartcalendar/service/StatisticsService.java index 4f01e3d..00ec665 100644 --- a/src/main/java/com/smartcalendar/service/StatisticsService.java +++ b/src/main/java/com/smartcalendar/service/StatisticsService.java @@ -27,7 +27,7 @@ public StatisticsData getStatistics(Long userId) { 0L, new TodayTimeDto(0, 0), new ContinuesSuccessDaysDto(0, 0), - new AverageDayTimeDto(0, 1) + new AverageDayTimeDto(0, null) ); } @@ -36,7 +36,7 @@ public StatisticsData getStatistics(Long userId) { stats.getWeekTime(), new TodayTimeDto(stats.getTodayPlanned(), stats.getTodayCompleted()), new ContinuesSuccessDaysDto(stats.getContinuesRecord(), stats.getContinuesNow()), - new AverageDayTimeDto(stats.getAverageWorkMinutes(), stats.getAverageTotalDays()) + new AverageDayTimeDto(stats.getAverageWorkMinutes(), stats.getFirstDay()) ); } @@ -65,7 +65,7 @@ public void updateStatistics(Long userId, StatisticsData statisticsData) { stats.setContinuesNow(statisticsData.getContinuesSuccessDays().getNow()); stats.setAverageWorkMinutes(statisticsData.getAverageDayTime().getTotalWorkMinutes()); - stats.setAverageTotalDays(statisticsData.getAverageDayTime().getTotalDays()); + stats.setFirstDay(statisticsData.getAverageDayTime().getFirstDay()); statisticsRepository.save(stats); } @@ -123,12 +123,12 @@ public AverageDayTimeDto getAverageDayTimeDto(Long userId) { .orElse(null); if (stats == null) { - return new AverageDayTimeDto(0, 0); + return new AverageDayTimeDto(0, null); } return new AverageDayTimeDto( stats.getAverageWorkMinutes(), - stats.getAverageTotalDays() + stats.getFirstDay() ); } } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/UserService.java b/src/main/java/com/smartcalendar/service/UserService.java index 84f3100..8aa3b65 100644 --- a/src/main/java/com/smartcalendar/service/UserService.java +++ b/src/main/java/com/smartcalendar/service/UserService.java @@ -6,6 +6,7 @@ import com.smartcalendar.model.Task; import com.smartcalendar.model.User; import com.smartcalendar.repository.EventRepository; +import com.smartcalendar.repository.StatisticsRepository; import com.smartcalendar.repository.TaskRepository; import com.smartcalendar.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -30,6 +31,7 @@ public class UserService { private final EventRepository eventRepository; private final PasswordEncoder passwordEncoder; private final StatisticsService statisticsService; + private final StatisticsRepository statisticsRepository; public User createUser(User user) { if (userRepository.existsByUsername(user.getUsername())) { @@ -259,4 +261,19 @@ public void editEvent(UUID eventId, Event event) { existingEvent.setCompleted(event.isCompleted()); eventRepository.save(existingEvent); } + + @Transactional + public void deleteAllUsersAndStatistics() { + statisticsRepository.deleteAll(); + userRepository.deleteAll(); + } + + @Transactional + public void deleteUser(Long userId) { + userRepository.deleteById(userId); + } + + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } } \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/config/TestSecurityConfig.java b/src/test/java/com/smartcalendar/config/TestSecurityConfig.java new file mode 100644 index 0000000..6be38ce --- /dev/null +++ b/src/test/java/com/smartcalendar/config/TestSecurityConfig.java @@ -0,0 +1,47 @@ +package com.smartcalendar.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.Collections; + +@TestConfiguration +public class TestSecurityConfig { + @Bean + @Primary + public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests((authz) -> authz.anyRequest().permitAll()); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public AuthenticationProvider testAuthenticationProvider() { + return new AuthenticationProvider() { + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + authentication.setAuthenticated(true); + return authentication; + } + + @Override + public boolean supports(Class authentication) { + return true; + } + }; + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/AudioControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/AudioControllerIntegrationTest.java new file mode 100644 index 0000000..aa8d869 --- /dev/null +++ b/src/test/java/com/smartcalendar/controller/AudioControllerIntegrationTest.java @@ -0,0 +1,80 @@ +package com.smartcalendar.controller; + +import com.smartcalendar.service.AudioProcessingService; +import com.smartcalendar.service.ChatGPTService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(properties = { + "JWT_SECRET=test_jwt_secret", + "chatgpt.api.url=http://dummy-url", + "chatgpt.api.key=dummy-key", + "spring.security.enabled=false" +}) +@AutoConfigureMockMvc +class AudioControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private AudioProcessingService audioProcessingService; + + @Autowired + private ChatGPTService chatGPTService; + + @TestConfiguration + static class MockConfig { + @Bean + public AudioProcessingService audioProcessingService() { + return Mockito.mock(AudioProcessingService.class); + } + @Bean + public ChatGPTService chatGPTService() { + return Mockito.mock(ChatGPTService.class); + } + } + + @Test + @WithMockUser + void testProcessAudio_Success() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "audio.wav", "audio/wav", "dummy".getBytes()); + + Mockito.when(audioProcessingService.transcribeAudio(any())).thenReturn("test transcript"); + Mockito.when(chatGPTService.processTranscript(any())).thenReturn( + Map.of("events", List.of(Map.of("title", "Event1")), "tasks", List.of()) + ); + Mockito.when(chatGPTService.convertToEntities(any())).thenReturn(List.of()); + + mockMvc.perform(multipart("/api/audio/process").file(file)) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void testProcessAudio_Error() throws Exception { + MockMultipartFile file = new MockMultipartFile("file", "audio.wav", "audio/wav", "dummy".getBytes()); + + Mockito.when(audioProcessingService.transcribeAudio(any())).thenThrow(new RuntimeException("Transcribe error")); + + mockMvc.perform(multipart("/api/audio/process").file(file)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").exists()); + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/AuthControllerTest.java b/src/test/java/com/smartcalendar/controller/AuthControllerTest.java index 2220bd5..b10d96d 100644 --- a/src/test/java/com/smartcalendar/controller/AuthControllerTest.java +++ b/src/test/java/com/smartcalendar/controller/AuthControllerTest.java @@ -11,6 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; @@ -21,6 +22,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.web.servlet.MockMvc; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -30,6 +33,7 @@ @SpringBootTest @AutoConfigureMockMvc +@Import(com.smartcalendar.config.TestSecurityConfig.class) @ActiveProfiles("test") @SpringJUnitConfig(AuthControllerTest.TestConfig.class) public class AuthControllerTest { @@ -67,10 +71,18 @@ public JwtService jwtService() { private User testUser; + private String registrationRequestJson(String username, String email, String password, String firstDay) throws Exception { + return objectMapper.writeValueAsString(Map.of( + "username", username, + "email", email, + "password", password, + "firstDay", firstDay + )); + } + @BeforeEach void setUp() { - // Cleanup and setup test user - userService.deleteAllUsers(); + userService.deleteAllUsersAndStatistics(); testUser = new User(); testUser.setUsername("testuser"); @@ -81,22 +93,29 @@ void setUp() { @Test void testRegisterUser_Success() throws Exception { - User newUser = new User(); - newUser.setUsername("newuser"); - newUser.setEmail("new@example.com"); - newUser.setPassword("Password123!"); - mockMvc.perform(post("/api/auth/signup") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(newUser))) + .content(registrationRequestJson("newuser", "new@example.com", "Password123!", "2025-06-07"))) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("newuser")) .andExpect(jsonPath("$.email").value("new@example.com")); } + @Test + void testRegisterUser_MissingFirstDay() throws Exception { + String json = objectMapper.writeValueAsString(Map.of( + "username", "user2", + "email", "user2@example.com", + "password", "Password123!" + )); + mockMvc.perform(post("/api/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()); + } + @Test void testLogin_Success() throws Exception { - // Mock authentication Authentication auth = new UsernamePasswordAuthenticationToken( testUser.getUsername(), testUser.getPassword() @@ -107,7 +126,6 @@ void testLogin_Success() throws Exception { when(jwtService.generateToken(testUser.getUsername())) .thenReturn("mocked.jwt.token"); - // Test request mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testUser))) @@ -132,13 +150,11 @@ void testLogin_InvalidCredentials() throws Exception { @Test void testLogin_MissingFields() throws Exception { - // Missing username mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content("{\"password\":\"password123\"}")) .andExpect(status().isBadRequest()); - // Missing password mockMvc.perform(post("/api/auth/login") .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"testuser\"}")) @@ -147,14 +163,9 @@ void testLogin_MissingFields() throws Exception { @Test void testRegisterUser_DuplicateUsername() throws Exception { - User duplicateUser = new User(); - duplicateUser.setUsername("testuser"); - duplicateUser.setEmail("duplicate@example.com"); - duplicateUser.setPassword("password123"); - mockMvc.perform(post("/api/auth/signup") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(duplicateUser))) + .content(registrationRequestJson("testuser", "duplicate@example.com", "password123", "2025-06-07"))) .andExpect(status().isBadRequest()); } @@ -168,7 +179,6 @@ void testCreateUser_PasswordIsHashed() { User savedUser = userService.createUser(user); assertNotEquals("rawPassword123", savedUser.getPassword()); - assertTrue(passwordEncoder.matches("rawPassword123", savedUser.getPassword())); } } \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/ChatGPTControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/ChatGPTControllerIntegrationTest.java new file mode 100644 index 0000000..77fe507 --- /dev/null +++ b/src/test/java/com/smartcalendar/controller/ChatGPTControllerIntegrationTest.java @@ -0,0 +1,80 @@ +package com.smartcalendar.controller; + +import com.smartcalendar.service.ChatGPTService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(properties = { + "JWT_SECRET=test_jwt_secret", + "chatgpt.api.url=http://dummy-url", + "chatgpt.api.key=dummy-key", + "spring.security.enabled=false" +}) +@AutoConfigureMockMvc +class ChatGPTControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ChatGPTService chatGPTService; + + @TestConfiguration + static class MockConfig { + @Bean + public ChatGPTService chatGPTService() { + return Mockito.mock(ChatGPTService.class); + } + } + + @Test + @WithMockUser + void testAskChatGPT() throws Exception { + Mockito.when(chatGPTService.askChatGPT(any(), any())).thenReturn("response"); + mockMvc.perform(post("/api/chatgpt/ask") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"question\":\"test?\"}")) + .andExpect(status().isOk()) + .andExpect(content().string("response")); + } + + @Test + @WithMockUser + void testGenerateEventsAndTasks() throws Exception { + Mockito.when(chatGPTService.generateEventsAndTasks(any())).thenReturn( + Map.of("events", List.of(), "tasks", List.of()) + ); + mockMvc.perform(post("/api/chatgpt/generate") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"query\":\"test\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.events").exists()) + .andExpect(jsonPath("$.tasks").exists()); + } + + @Test + @WithMockUser + void testGenerateEntities_Error() throws Exception { + Mockito.when(chatGPTService.processTranscript(any())).thenReturn(Map.of("error", "Unrelated request")); + mockMvc.perform(post("/api/chatgpt/generate/entities") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"query\":\"test\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").exists()); + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java new file mode 100644 index 0000000..3f55d1a --- /dev/null +++ b/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java @@ -0,0 +1,104 @@ +package com.smartcalendar.controller; + +import com.smartcalendar.dto.*; +import com.smartcalendar.model.User; +import com.smartcalendar.service.StatisticsService; +import com.smartcalendar.service.UserService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(properties = { + "JWT_SECRET=test_jwt_secret" +}) +@ActiveProfiles("test") +@AutoConfigureMockMvc +@Import(com.smartcalendar.config.TestSecurityConfig.class) +class StatisticsControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private StatisticsService statisticsService; + + @Autowired + private UserService userService; + + @TestConfiguration + static class MockConfig { + @Bean + public StatisticsService statisticsService() { + return Mockito.mock(StatisticsService.class); + } + @Bean + public UserService userService() { + return Mockito.mock(UserService.class); + } + } + + private void mockAuth() { + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + } + + @Test + @WithMockUser + void testGetTotalTimeTaskTypes() throws Exception { + mockAuth(); + Mockito.when(statisticsService.getTotalTimeTaskTypes(any())).thenReturn(new TotalTimeTaskTypesDto(1,2,3,4)); + mockMvc.perform(get("/api/statistics/total-time-task-types")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.common").value(1)); + } + + @Test + @WithMockUser + void testGetTodayTimeDto() throws Exception { + mockAuth(); + Mockito.when(statisticsService.getTodayTimeDto(any())).thenReturn(new TodayTimeDto(5,6)); + mockMvc.perform(get("/api/statistics/today")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.planned").value(5)); + } + + @Test + @WithMockUser + void testGetContinuesSuccessDaysDto() throws Exception { + mockAuth(); + Mockito.when(statisticsService.getContinuesSuccessDaysDto(any())).thenReturn(new ContinuesSuccessDaysDto(7,8)); + mockMvc.perform(get("/api/statistics/continuous-success-days")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.record").value(7)); + } + + @Test + @WithMockUser + void testGetAverageDayTimeDto() throws Exception { + mockAuth(); + Mockito.when(statisticsService.getAverageDayTimeDto(any())).thenReturn(new AverageDayTimeDto(9, null)); + mockMvc.perform(get("/api/statistics/average-day-time")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalWorkMinutes").value(9)); + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java new file mode 100644 index 0000000..e663308 --- /dev/null +++ b/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java @@ -0,0 +1,90 @@ +package com.smartcalendar.controller; + +import com.smartcalendar.model.User; +import com.smartcalendar.service.UserService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(properties = { + "JWT_SECRET=test_jwt_secret", + "spring.security.enabled=false" +}) +@ActiveProfiles("test") +@AutoConfigureMockMvc +@Import(com.smartcalendar.config.TestSecurityConfig.class) +class UserControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserService userService; + + @TestConfiguration + static class MockConfig { + @Bean + public UserService userService() { + return Mockito.mock(UserService.class); + } + } + + @Test + @WithMockUser + void testGetAllUsers() throws Exception { + Mockito.when(userService.findAllUsers()).thenReturn(Collections.emptyList()); + mockMvc.perform(get("/api/users")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void testGetUserById() throws Exception { + User user = new User(); + user.setId(1L); + user.setUsername("testuser"); + Mockito.when(userService.findUserById(1L)).thenReturn(user); + mockMvc.perform(get("/api/users/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + @WithMockUser + void testCreateUser() throws Exception { + User user = new User(); + user.setId(2L); + user.setUsername("newuser"); + Mockito.when(userService.createUser(any(User.class))).thenReturn(user); + + String json = """ + { + "username": "newuser", + "email": "newuser@example.com", + "password": "Password123!" + } + """; + mockMvc.perform(post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("newuser")); + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/service/ChatGPTServiceTest.java b/src/test/java/com/smartcalendar/service/ChatGPTServiceTest.java new file mode 100644 index 0000000..8de75c4 --- /dev/null +++ b/src/test/java/com/smartcalendar/service/ChatGPTServiceTest.java @@ -0,0 +1,53 @@ +package com.smartcalendar.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ChatGPTServiceTest { + + @InjectMocks + private ChatGPTService chatGPTService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testConvertToEntities() { + Map> data = Map.of( + "events", List.of(Map.of("title", "Event1")), + "tasks", List.of(Map.of("title", "Task1", "completed", false)) + ); + var entities = chatGPTService.convertToEntities(data); + assertEquals(2, entities.size()); + assertTrue(entities.stream().anyMatch(e -> e.getClass().getSimpleName().equals("Event"))); + assertTrue(entities.stream().anyMatch(e -> e.getClass().getSimpleName().equals("Task"))); + } + + @Test + void testProcessTranscript_Error() { + ChatGPTService spyService = spy(chatGPTService); + doReturn("{\"error\": \"Unrelated request\"}").when(spyService).askChatGPT(anyString(), anyString()); + Map result = spyService.processTranscript("some unrelated text"); + assertTrue(result.containsKey("error")); + } + + @Test + void testGenerateEventsAndTasks_ValidJson() { + ChatGPTService spyService = spy(chatGPTService); + doReturn("{\"events\":[],\"tasks\":[]}").when(spyService).askChatGPT(anyString(), anyString()); + Map> result = spyService.generateEventsAndTasks("test"); + assertTrue(result.containsKey("events")); + assertTrue(result.containsKey("tasks")); + } +} \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java b/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java new file mode 100644 index 0000000..691dbfa --- /dev/null +++ b/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java @@ -0,0 +1,168 @@ +package com.smartcalendar.service; + +import com.smartcalendar.dto.*; +import com.smartcalendar.model.Statistics; +import com.smartcalendar.model.User; +import com.smartcalendar.repository.StatisticsRepository; +import com.smartcalendar.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; +import java.time.LocalDate; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class StatisticsServiceTest { + + @Mock + private StatisticsRepository statisticsRepository; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private StatisticsService statisticsService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testGetStatistics_WhenStatsExist() { + Statistics stats = new Statistics(); + stats.setTotalCommon(1); + stats.setTotalWork(2); + stats.setTotalStudy(3); + stats.setTotalFitness(4); + stats.setWeekTime(5); + stats.setTodayPlanned(6); + stats.setTodayCompleted(7); + stats.setContinuesRecord(8); + stats.setContinuesNow(9); + stats.setAverageWorkMinutes(10); + stats.setFirstDay(LocalDate.of(2024, 1, 1)); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + + StatisticsData data = statisticsService.getStatistics(1L); + + assertEquals(1, data.getTotalTime().getCommon()); + assertEquals(2, data.getTotalTime().getWork()); + assertEquals(3, data.getTotalTime().getStudy()); + assertEquals(4, data.getTotalTime().getFitness()); + assertEquals(5, data.getWeekTime()); + assertEquals(6, data.getTodayTime().getPlanned()); + assertEquals(7, data.getTodayTime().getCompleted()); + assertEquals(8, data.getContinuesSuccessDays().getRecord()); + assertEquals(9, data.getContinuesSuccessDays().getNow()); + assertEquals(10, data.getAverageDayTime().getTotalWorkMinutes()); + assertEquals(LocalDate.of(2024, 1, 1), data.getAverageDayTime().getFirstDay()); + } + + @Test + void testGetStatistics_WhenStatsNotExist() { + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.empty()); + StatisticsData data = statisticsService.getStatistics(1L); + assertNotNull(data); + assertEquals(0, data.getTotalTime().getCommon()); + assertNull(data.getAverageDayTime().getFirstDay()); + } + + @Test + void testUpdateStatistics_NewStats() { + User user = new User(); + user.setId(1L); + + StatisticsData dto = new StatisticsData( + new TotalTimeTaskTypesDto(1, 2, 3, 4), + 5L, + new TodayTimeDto(6, 7), + new ContinuesSuccessDaysDto(8, 9), + new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)) + ); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.empty()); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(statisticsRepository.save(any(Statistics.class))).thenAnswer(i -> i.getArgument(0)); + + assertDoesNotThrow(() -> statisticsService.updateStatistics(1L, dto)); + verify(statisticsRepository).save(any(Statistics.class)); + } + + @Test + void testUpdateStatistics_ExistingStats() { + User user = new User(); + user.setId(1L); + Statistics stats = new Statistics(); + stats.setUser(user); + + StatisticsData dto = new StatisticsData( + new TotalTimeTaskTypesDto(1, 2, 3, 4), + 5L, + new TodayTimeDto(6, 7), + new ContinuesSuccessDaysDto(8, 9), + new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)) + ); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + when(statisticsRepository.save(any(Statistics.class))).thenAnswer(i -> i.getArgument(0)); + + assertDoesNotThrow(() -> statisticsService.updateStatistics(1L, dto)); + verify(statisticsRepository).save(any(Statistics.class)); + } + + @Test + void testGetTotalTimeTaskTypes() { + Statistics stats = new Statistics(); + stats.setTotalCommon(1); + stats.setTotalWork(2); + stats.setTotalStudy(3); + stats.setTotalFitness(4); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + TotalTimeTaskTypesDto dto = statisticsService.getTotalTimeTaskTypes(1L); + assertEquals(1, dto.getCommon()); + assertEquals(2, dto.getWork()); + assertEquals(3, dto.getStudy()); + assertEquals(4, dto.getFitness()); + } + + @Test + void testGetTodayTimeDto() { + Statistics stats = new Statistics(); + stats.setTodayPlanned(5); + stats.setTodayCompleted(6); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + TodayTimeDto dto = statisticsService.getTodayTimeDto(1L); + assertEquals(5, dto.getPlanned()); + assertEquals(6, dto.getCompleted()); + } + + @Test + void testGetContinuesSuccessDaysDto() { + Statistics stats = new Statistics(); + stats.setContinuesRecord(7); + stats.setContinuesNow(8); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + ContinuesSuccessDaysDto dto = statisticsService.getContinuesSuccessDaysDto(1L); + assertEquals(7, dto.getRecord()); + assertEquals(8, dto.getNow()); + } + + @Test + void testGetAverageDayTimeDto() { + Statistics stats = new Statistics(); + stats.setAverageWorkMinutes(9); + stats.setFirstDay(LocalDate.of(2024, 2, 2)); + + when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); + AverageDayTimeDto dto = statisticsService.getAverageDayTimeDto(1L); + assertEquals(9, dto.getTotalWorkMinutes()); + assertEquals(LocalDate.of(2024, 2, 2), dto.getFirstDay()); + } +} diff --git a/src/test/java/com/smartcalendar/service/UserServiceTest.java b/src/test/java/com/smartcalendar/service/UserServiceTest.java index baee1ba..1cf23b3 100644 --- a/src/test/java/com/smartcalendar/service/UserServiceTest.java +++ b/src/test/java/com/smartcalendar/service/UserServiceTest.java @@ -10,6 +10,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Optional; +import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -64,20 +66,75 @@ void testFindUserById() { } @Test - void testGetCurrentUserId() { + void testFindByUsername() { User user = new User(); - user.setId(1L); user.setUsername("testuser"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(user)); - String username = "testuser"; + Optional found = userService.findByUsername("testuser"); + assertTrue(found.isPresent()); + assertEquals("testuser", found.get().getUsername()); + } + + @Test + void testFindByEmail() { + User user = new User(); + user.setEmail("test@example.com"); + + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(user)); + + Optional found = userService.findByEmail("test@example.com"); + assertTrue(found.isPresent()); + assertEquals("test@example.com", found.get().getEmail()); + } + + @Test + void testExistsByUsername() { + when(userRepository.existsByUsername("testuser")).thenReturn(true); + assertTrue(userService.existsByUsername("testuser")); + } + + @Test + void testExistsByEmail() { + when(userRepository.existsByEmail("test@example.com")).thenReturn(true); + assertTrue(userService.existsByEmail("test@example.com")); + } + + @Test + void testUpdateEmail() { + User user = new User(); + user.setId(1L); + user.setEmail("old@example.com"); + when(userRepository.findById(1L)).thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))).thenReturn(user); - User foundUser = userService.findUserById(1L); + User updated = userService.updateEmail(1L, "new@example.com"); + assertEquals("new@example.com", updated.getEmail()); + } - assertNotNull(foundUser); - assertEquals(1L, foundUser.getId()); - assertEquals("testuser", foundUser.getUsername()); + @Test + void testFindAllUsers() { + User user = new User(); + user.setUsername("testuser"); + when(userRepository.findAll()).thenReturn(Collections.singletonList(user)); + + List users = userService.findAllUsers(); + assertEquals(1, users.size()); + assertEquals("testuser", users.get(0).getUsername()); + } + + @Test + void testDeleteUser() { + doNothing().when(userRepository).deleteById(1L); + userService.deleteUser(1L); + verify(userRepository).deleteById(1L); + } + + @Test + void testFindUserById_NotFound() { + when(userRepository.findById(99L)).thenReturn(Optional.empty()); + assertThrows(RuntimeException.class, () -> userService.findUserById(99L)); } } \ No newline at end of file From 772805b1e0e0514eb7f14209a910ccdf0b397051 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Sat, 7 Jun 2025 17:05:08 +0300 Subject: [PATCH 6/8] CI test log added --- .github/workflows/ci.yml | 2 +- build.gradle | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index def0487..d53356c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: run: chmod +x ./gradlew - name: Build and test - run: ./gradlew clean build + run: ./gradlew clean build --info --console=plain - name: Generate license report run: ./gradlew generateLicenseReport diff --git a/build.gradle b/build.gradle index 38e3807..cd2eb5c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' id 'com.github.jk1.dependency-license-report' version '2.8' + id 'com.adarshr.test-logger' version '4.0.0' } group = 'com.smartcalendar' @@ -14,6 +15,10 @@ java { } } +testlogger { + theme 'mocha' +} + repositories { mavenCentral() } From 409fd43a5a5c8ee44efc60170936c098e9ac8cb4 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Mon, 9 Jun 2025 15:14:22 +0300 Subject: [PATCH 7/8] some minor additions --- .../controller/ChatGPTController.java | 9 +- .../com/smartcalendar/dto/StatisticsData.java | 3 + .../smartcalendar/service/ChatGPTService.java | 84 +++++++- .../service/StatisticsService.java | 10 +- .../StatisticsControllerIntegrationTest.java | 21 ++ .../UserControllerIntegrationTest.java | 180 ++++++++++++++++-- .../service/StatisticsServiceTest.java | 11 +- 7 files changed, 289 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/smartcalendar/controller/ChatGPTController.java b/src/main/java/com/smartcalendar/controller/ChatGPTController.java index e1cd83e..3dfad32 100644 --- a/src/main/java/com/smartcalendar/controller/ChatGPTController.java +++ b/src/main/java/com/smartcalendar/controller/ChatGPTController.java @@ -41,13 +41,12 @@ public ResponseEntity generateEntities(@RequestBody Map reque return ResponseEntity.badRequest().body(response); } - if (!(response.get("events") instanceof List) || !(response.get("tasks") instanceof List)) { - return ResponseEntity.badRequest().body(Map.of("error", "Invalid response format from ChatGPT")); - } + List events = response.get("events") instanceof List ? (List) response.get("events") : List.of(); + List tasks = response.get("tasks") instanceof List ? (List) response.get("tasks") : List.of(); Map> validResponse = Map.of( - "events", (List) response.get("events"), - "tasks", (List) response.get("tasks") + "events", events, + "tasks", tasks ); List entities = chatGPTService.convertToEntities(validResponse); diff --git a/src/main/java/com/smartcalendar/dto/StatisticsData.java b/src/main/java/com/smartcalendar/dto/StatisticsData.java index c370a85..97f0b86 100644 --- a/src/main/java/com/smartcalendar/dto/StatisticsData.java +++ b/src/main/java/com/smartcalendar/dto/StatisticsData.java @@ -4,6 +4,8 @@ import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; +import java.util.Date; + @Data @NoArgsConstructor @AllArgsConstructor @@ -13,4 +15,5 @@ public class StatisticsData { private TodayTimeDto todayTime = new TodayTimeDto(0, 0); private ContinuesSuccessDaysDto continuesSuccessDays = new ContinuesSuccessDaysDto(0, 0); private AverageDayTimeDto averageDayTime = new AverageDayTimeDto(0, null); + private Date jsonDate = new Date(); } \ No newline at end of file diff --git a/src/main/java/com/smartcalendar/service/ChatGPTService.java b/src/main/java/com/smartcalendar/service/ChatGPTService.java index 0adaa9d..6926e17 100644 --- a/src/main/java/com/smartcalendar/service/ChatGPTService.java +++ b/src/main/java/com/smartcalendar/service/ChatGPTService.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.smartcalendar.model.Event; +import com.smartcalendar.model.EventType; import com.smartcalendar.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,9 +15,9 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; @Service public class ChatGPTService { @@ -87,9 +88,23 @@ public Map> generateEventsAndTasks(String userQuery) { logger.info("Generating events and tasks for query: {}", userQuery); String prompt = "Based on the user's query: \"" + userQuery + "\", generate a list of events and tasks. " + - "Respond in JSON format with the following structure: " + - "{ \"events\": [{ \"title\": \"string\", \"start\": \"ISO 8601 datetime\", \"end\": \"ISO 8601 datetime\", \"location\": \"string\" }], " + - "\"tasks\": [{ \"title\": \"string\", \"description\": \"string\", \"completed\": false }] }"; + "Respond strictly in JSON format with the following structure: " + + "{ \"events\": [{ " + + "\"title\": \"string\", " + + "\"description\": \"string\", " + + "\"start\": \"ISO 8601 datetime\", " + + "\"end\": \"ISO 8601 datetime\", " + + "\"location\": \"string\", " + + "\"type\": \"COMMON|FITNESS|STUDIES|WORK\" " + + "}], " + + "\"tasks\": [{ " + + "\"title\": \"string\", " + + "\"description\": \"string\", " + + "\"completed\": false, " + + "\"dueDateTime\": \"ISO 8601 datetime\", " + + "\"allDay\": false " + + "}] } " + + "Do not include any additional text or explanation."; String response = askChatGPT(prompt, "gpt-3.5-turbo"); @@ -112,6 +127,21 @@ public List convertToEntities(Map> data) { if (events != null) { for (Map eventData : events) { Event event = objectMapper.convertValue(eventData, Event.class); + + if (event.getId() == null) { + event.setId(UUID.randomUUID()); + } + if (event.getCreationTime() == null) { + event.setCreationTime(LocalDateTime.now()); + } + if (event.getType() == null && eventData.get("type") != null) { + try { + event.setType(EventType.valueOf(eventData.get("type").toString())); + } catch (Exception ignored) {} + } + if (!event.isCompleted() && eventData.get("completed") != null) { + event.setCompleted(Boolean.parseBoolean(eventData.get("completed").toString())); + } entities.add(event); } } @@ -119,6 +149,22 @@ public List convertToEntities(Map> data) { if (tasks != null) { for (Map taskData : tasks) { Task task = objectMapper.convertValue(taskData, Task.class); + + if (task.getId() == null) { + task.setId(UUID.randomUUID()); + } + if (task.getCreationTime() == null) { + task.setCreationTime(LocalDateTime.now()); + } + if (task.getAllDay() == null && taskData.get("allDay") != null) { + task.setAllDay(Boolean.parseBoolean(taskData.get("allDay").toString())); + } + if (task.getDueDateTime() == null && taskData.get("dueDate") != null) { + try { + LocalDate date = LocalDate.parse(taskData.get("dueDate").toString()); + task.setDueDateTime(date.atStartOfDay()); + } catch (Exception ignored) {} + } entities.add(task); } } @@ -127,10 +173,24 @@ public List convertToEntities(Map> data) { } public Map processTranscript(String transcript) { - String prompt = "Based on the following transcript: \"" + transcript + "\", determine if it is related to creating events or tasks. " + + String today = LocalDate.now().toString(); + String prompt = "Today is " + today + ". Based on the following transcript: \"" + transcript + "\", determine if it is related to creating events or tasks. " + "If it is, generate a list of events and tasks strictly in JSON format with the following structure: " + - "{ \"events\": [{ \"title\": \"string\", \"start\": \"ISO 8601 datetime\", \"end\": \"ISO 8601 datetime\", \"location\": \"string\", \"description\": \"string\", \"type\": \"string\" }], " + - "\"tasks\": [{ \"title\": \"string\", \"description\": \"string\", \"completed\": false, \"dueDate\": \"ISO 8601 date\" }] }. " + + "{ \"events\": [{ " + + "\"title\": \"string\", " + + "\"description\": \"string\", " + + "\"start\": \"ISO 8601 datetime\", " + + "\"end\": \"ISO 8601 datetime\", " + + "\"location\": \"string\", " + + "\"type\": \"COMMON|FITNESS|STUDIES|WORK\" " + + "}], " + + "\"tasks\": [{ " + + "\"title\": \"string\", " + + "\"description\": \"string\", " + + "\"completed\": false, " + + "\"dueDateTime\": \"ISO 8601 datetime\", " + + "\"allDay\": false " + + "}] } " + "If the transcript is not related to events or tasks, respond with: { \"error\": \"Unrelated request\" }. " + "Do not include any additional text or explanation."; @@ -141,6 +201,12 @@ public Map processTranscript(String transcript) { if (result.containsKey("error")) { logger.warn("ChatGPT returned an error: {}", result); } + if (!result.containsKey("events")) { + result.put("events", List.of()); + } + if (!result.containsKey("tasks")) { + result.put("tasks", List.of()); + } return result; } catch (Exception e) { throw new RuntimeException("Failed to process ChatGPT response: " + e.getMessage()); diff --git a/src/main/java/com/smartcalendar/service/StatisticsService.java b/src/main/java/com/smartcalendar/service/StatisticsService.java index 00ec665..562d2fc 100644 --- a/src/main/java/com/smartcalendar/service/StatisticsService.java +++ b/src/main/java/com/smartcalendar/service/StatisticsService.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Date; + @Service @RequiredArgsConstructor public class StatisticsService { @@ -21,13 +23,16 @@ public StatisticsData getStatistics(Long userId) { Statistics stats = statisticsRepository.findByUserId(userId) .orElse(null); + Date now = new Date(); + if (stats == null) { return new StatisticsData( new TotalTimeTaskTypesDto(0, 0, 0, 0), 0L, new TodayTimeDto(0, 0), new ContinuesSuccessDaysDto(0, 0), - new AverageDayTimeDto(0, null) + new AverageDayTimeDto(0, null), + now ); } @@ -36,7 +41,8 @@ public StatisticsData getStatistics(Long userId) { stats.getWeekTime(), new TodayTimeDto(stats.getTodayPlanned(), stats.getTodayCompleted()), new ContinuesSuccessDaysDto(stats.getContinuesRecord(), stats.getContinuesNow()), - new AverageDayTimeDto(stats.getAverageWorkMinutes(), stats.getFirstDay()) + new AverageDayTimeDto(stats.getAverageWorkMinutes(), stats.getFirstDay()), + now ); } diff --git a/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java index 3f55d1a..dc80247 100644 --- a/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java +++ b/src/test/java/com/smartcalendar/controller/StatisticsControllerIntegrationTest.java @@ -19,6 +19,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import java.util.Date; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -101,4 +102,24 @@ void testGetAverageDayTimeDto() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.totalWorkMinutes").value(9)); } + + @Test + @WithMockUser + void testGetStatisticsWithJsonDate() throws Exception { + mockAuth(); + StatisticsData statisticsData = new StatisticsData( + new TotalTimeTaskTypesDto(1, 2, 3, 4), + 5L, + new TodayTimeDto(6, 7), + new ContinuesSuccessDaysDto(8, 9), + new AverageDayTimeDto(10, null), + new Date() + ); + Mockito.when(userService.getStatistics(any())).thenReturn(statisticsData); + + mockMvc.perform(get("/api/users/1/statistics")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalTime.common").value(1)) + .andExpect(jsonPath("$.jsonDate").exists()); + } } \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java b/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java index e663308..d8905ea 100644 --- a/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java +++ b/src/test/java/com/smartcalendar/controller/UserControllerIntegrationTest.java @@ -1,5 +1,11 @@ package com.smartcalendar.controller; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.smartcalendar.dto.DailyTaskDto; +import com.smartcalendar.dto.StatisticsData; +import com.smartcalendar.model.Event; +import com.smartcalendar.model.EventType; +import com.smartcalendar.model.Task; import com.smartcalendar.model.User; import com.smartcalendar.service.UserService; import org.junit.jupiter.api.Test; @@ -11,16 +17,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import java.util.Collections; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest(properties = { @@ -38,6 +46,9 @@ class UserControllerIntegrationTest { @Autowired private UserService userService; + @Autowired + private ObjectMapper objectMapper; + @TestConfiguration static class MockConfig { @Bean @@ -46,6 +57,14 @@ public UserService userService() { } } + private User mockUser(Long id, String username) { + User user = new User(); + user.setId(id); + user.setUsername(username); + user.setEmail(username + "@example.com"); + return user; + } + @Test @WithMockUser void testGetAllUsers() throws Exception { @@ -57,9 +76,7 @@ void testGetAllUsers() throws Exception { @Test @WithMockUser void testGetUserById() throws Exception { - User user = new User(); - user.setId(1L); - user.setUsername("testuser"); + User user = mockUser(1L, "testuser"); Mockito.when(userService.findUserById(1L)).thenReturn(user); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) @@ -69,9 +86,7 @@ void testGetUserById() throws Exception { @Test @WithMockUser void testCreateUser() throws Exception { - User user = new User(); - user.setId(2L); - user.setUsername("newuser"); + User user = mockUser(2L, "newuser"); Mockito.when(userService.createUser(any(User.class))).thenReturn(user); String json = """ @@ -87,4 +102,147 @@ void testCreateUser() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("newuser")); } + + @Test + @WithMockUser + void testUpdateEmail() throws Exception { + User user = mockUser(1L, "testuser"); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.updateEmail(eq(1L), anyString())).thenReturn(user); + + mockMvc.perform(put("/api/users/1/email") + .contentType(MediaType.APPLICATION_JSON) + .content("\"newemail@example.com\"")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + @WithMockUser + void testGetTasksByUserId() throws Exception { + User user = mockUser(1L, "testuser"); + Task task = new Task(); + task.setId(UUID.randomUUID()); + task.setUser(user); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.findTasksByUserId(1L)).thenReturn(List.of(task)); + + mockMvc.perform(get("/api/users/1/tasks")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").exists()); + } + + @Test + @WithMockUser + void testGetEventsByUserId() throws Exception { + User user = mockUser(1L, "testuser"); + Event event = new Event(); + event.setId(UUID.randomUUID()); + event.setOrganizer(user); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.findEventsByUserId(1L)).thenReturn(List.of(event)); + + mockMvc.perform(get("/api/users/1/events")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").exists()); + } + + @Test + @WithMockUser + void testCreateEvent() throws Exception { + User user = mockUser(1L, "testuser"); + Event event = new Event(); + event.setId(UUID.randomUUID()); + event.setOrganizer(user); + + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.createEventWithCustomId(any(Event.class))).thenReturn(event); + + String json = objectMapper.writeValueAsString(event); + mockMvc.perform(post("/api/users/1/events") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()); + } + + @Test + @WithMockUser + void testCreateTask() throws Exception { + User user = mockUser(1L, "testuser"); + Task task = new Task(); + task.setId(UUID.randomUUID()); + task.setUser(user); + + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.createTaskWithCustomId(any(Task.class))).thenReturn(task); + + String json = objectMapper.writeValueAsString(task); + mockMvc.perform(post("/api/users/1/tasks") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()); + } + + @Test + @WithMockUser + void testGetCurrentUserInfo() throws Exception { + User user = mockUser(1L, "testuser"); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + + mockMvc.perform(get("/api/users/me")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("testuser")); + } + + @Test + @WithMockUser + void testGetAllEventsAsDailyTasks() throws Exception { + User user = mockUser(1L, "testuser"); + DailyTaskDto dailyTask = new DailyTaskDto( + UUID.randomUUID(), + "Test Task", + false, + EventType.WORK, + LocalDateTime.now(), + "desc", + LocalTime.of(9, 0), + LocalTime.of(10, 0), + LocalDate.now() + ); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.findAllEventsAsDailyTaskDto(1L)).thenReturn(List.of(dailyTask)); + + mockMvc.perform(get("/api/users/1/events/dailytasks")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0]").exists()); + } + + @Test + @WithMockUser + void testGetStatistics() throws Exception { + User user = mockUser(1L, "testuser"); + StatisticsData statisticsData = new StatisticsData(); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + Mockito.when(userService.getStatistics(1L)).thenReturn(statisticsData); + + mockMvc.perform(get("/api/users/1/statistics")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalTime").exists()); + } + + @Test + @WithMockUser + void testUpdateStatistics() throws Exception { + User user = mockUser(1L, "testuser"); + StatisticsData statisticsData = new StatisticsData(); + Mockito.when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + + String json = objectMapper.writeValueAsString(statisticsData); + mockMvc.perform(put("/api/users/1/statistics") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()); + } } \ No newline at end of file diff --git a/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java b/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java index 691dbfa..0e0e3c0 100644 --- a/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java +++ b/src/test/java/com/smartcalendar/service/StatisticsServiceTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.mockito.*; import java.time.LocalDate; +import java.util.Date; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -60,6 +61,8 @@ void testGetStatistics_WhenStatsExist() { assertEquals(9, data.getContinuesSuccessDays().getNow()); assertEquals(10, data.getAverageDayTime().getTotalWorkMinutes()); assertEquals(LocalDate.of(2024, 1, 1), data.getAverageDayTime().getFirstDay()); + assertNotNull(data.getJsonDate()); // Новая проверка + assertTrue(data.getJsonDate().getTime() <= new Date().getTime()); // jsonDate не в будущем } @Test @@ -69,6 +72,8 @@ void testGetStatistics_WhenStatsNotExist() { assertNotNull(data); assertEquals(0, data.getTotalTime().getCommon()); assertNull(data.getAverageDayTime().getFirstDay()); + assertNotNull(data.getJsonDate()); // Новая проверка + assertTrue(data.getJsonDate().getTime() <= new Date().getTime()); } @Test @@ -81,7 +86,8 @@ void testUpdateStatistics_NewStats() { 5L, new TodayTimeDto(6, 7), new ContinuesSuccessDaysDto(8, 9), - new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)) + new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)), + new Date() ); when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.empty()); @@ -104,7 +110,8 @@ void testUpdateStatistics_ExistingStats() { 5L, new TodayTimeDto(6, 7), new ContinuesSuccessDaysDto(8, 9), - new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)) + new AverageDayTimeDto(10, LocalDate.of(2024, 1, 1)), + new Date() ); when(statisticsRepository.findByUserId(1L)).thenReturn(Optional.of(stats)); From c2f6d69cf4095721dc5851c3065ab5246f689710 Mon Sep 17 00:00:00 2001 From: DimaRus05 Date: Tue, 10 Jun 2025 19:21:03 +0300 Subject: [PATCH 8/8] README.md & LICENSE.txt added --- LICENSE.txt | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 155 +++++++++++- 2 files changed, 828 insertions(+), 1 deletion(-) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 675478f..9323068 100644 --- a/README.md +++ b/README.md @@ -1 +1,154 @@ -# server \ No newline at end of file +# "TimeTamer" SmartCalendar Server + +Spring Boot backend for the "TimeTamer" SmartCalendar application. Provides REST API for task/event management, user statistics, and OpenAI integration. + +--- + +## Key Features +- **User Management**: Registration, authentication, and profile updates +- **Task & Event Operations**: Full CRUD functionality with status tracking +- **JWT Authentication**: Secure token-based access control +- **OpenAI Integration**: + - ChatGPT for natural language processing + - Whisper for speech-to-text +- **Automated Documentation**: Swagger UI for interactive API exploration +- **CI/CD Pipeline**: GitHub Actions for automated testing and deployment + +--- + +## Tech Stack +- **Language**: Java 21 +- **Framework**: Spring Boot 3.1+ +- **Database**: + - PostgreSQL (Production) + - H2 (Development/Testing) +- **Build Tool**: Gradle 8+ + +--- + +## Prerequisites +- Java 21 JDK +- Gradle 8+ +- PostgreSQL 15+ (for production) +- OpenAI API key + +--- + +## Quick Start (Development) +1. Clone repository: + ```bash + git clone https://github.com/hse-project-Java-2025/server.git + cd smartcalendar-server + ``` +2. Set environment variables (create `.env` file): + ```ini + JWT_SECRET=your_strong_secret_here + CHATGPT_API_KEY=your_openai_api_key + ``` +3. Build and run: + ```bash + ./gradlew bootRun + ``` +4. Access resources: + - **Swagger UI**: `http://localhost:8080/swagger-ui.html` (complete API documentation) + - H2 Console: `http://localhost:8080/h2-console` (JDBC URL: `jdbc:h2:mem:testdb`) + +--- + +## Configuration +### Essential Environment Variables +| Variable | Description | Example | +|-------------------|-------------------------------------|-----------------------------| +| `JWT_SECRET` | Secret for JWT token signing | `A$ecretKey!123` | +| `CHATGPT_API_KEY` | OpenAI API key | `sk-...` | +| `DB_URL` | Production DB URL (optional) | `jdbc:postgresql://db:5432` | + +### Production Setup +1. Create `application-prod.properties`: + ```properties + spring.datasource.url=jdbc:postgresql://your-db-host:5432/smartcalendar + spring.datasource.username=dbuser + spring.datasource.password=dbpassword + spring.jpa.hibernate.ddl-auto=update + ``` +2. Build executable JAR: + ```bash + ./gradlew clean build + ``` +3. Run with production profile: + ```bash + java -Dspring.profiles.active=prod -jar build/libs/smartcalendar-*.jar + ``` + +--- + +## API Reference +### Core Endpoints Overview +Below are representative examples of available API endpoints. Complete and always up-to-date definitive API reference is automatically generated at runtime: +```http +http://localhost:8080/swagger-ui.html +``` + + +### User Management +| Endpoint | Method | Description | +|-----------------------------------|--------|------------------------------| +| `/api/users` | GET | List all users | +| `/api/users` | POST | Register new user | +| `/api/users/{id}` | GET | Get user details | +| `/api/users/{id}/email` | PUT | Update user email | +| `/api/users/{userId}/statistics` | GET | Get user statistics | + +### Task Management +| Endpoint | Method | Description | +|---------------------------------------|--------|------------------------------| +| `/api/users/{userId}/tasks` | GET | Get user's tasks | +| `/api/users/{userId}/tasks` | POST | Create new task | +| `/api/users/tasks/{taskId}/status` | PATCH | Update task status | +| `/api/users/tasks/{taskId}` | DELETE | Delete task | + +### Event Management +| Endpoint | Method | Description | +|---------------------------------------|--------|------------------------------| +| `/api/users/{userId}/events` | GET | Get user's events | +| `/api/users/{userId}/events` | POST | Create new event | +| `/api/users/events/{eventId}` | PATCH | Update event | +| `/api/users/events/{eventId}` | DELETE | Delete event | + +### OpenAI Integration +| Endpoint | Method | Description | +|-------------------------------|--------|--------------------------------------| +| `/api/chatgpt/ask` | POST | Get ChatGPT response | +| `/api/chatgpt/generate` | POST | Generate calendar events/tasks | +| `/api/chatgpt/generate/entities` | POST | Generate entities from natural language | + +--- + +## Testing +Run tests with: +```bash +./gradlew test +``` +- Uses separate in-memory H2 database +- External services (OpenAI) are mocked +- Test coverage reports: `build/reports/tests` + +--- + +## CI/CD Pipeline +GitHub Actions workflow (`.github/workflows/ci.yml`): +1. Build with JDK 21 +2. Run all tests +3. Generate dependency license report +4. Upload test reports as artifacts + +--- + +## License +MIT License - see [LICENSE](LICENSE.txt) file + +--- + +## Contributors +- [Dmitry Rusanov](https://github.com/DimaRus05) +- [Mikhail Minaev](https://github.com/minmise)