From 0372249fea1d639830b3415ccc59028d175918cd Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Sat, 31 Jan 2026 14:08:27 +0100 Subject: [PATCH 01/14] Update test dependencies and Add tests to check the first exceptions in BookingSystem class --- pom.xml | 9 +- .../java/com/example/BookingSystemTest.java | 102 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/example/BookingSystemTest.java diff --git a/pom.xml b/pom.xml index 543c1aa50..a3aa79e17 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ org.junit.jupiter junit-jupiter - 5.11.4 + 6.0.1 test @@ -28,9 +28,14 @@ org.mockito mockito-junit-jupiter - 5.15.2 + 5.21.0 test + + org.awaitility + awaitility + 4.3.0 + diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java new file mode 100644 index 000000000..d5ecde87d --- /dev/null +++ b/src/test/java/com/example/BookingSystemTest.java @@ -0,0 +1,102 @@ +package com.example; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +public class BookingSystemTest { + + @Mock + TimeProvider timeProvider; + + @Mock + RoomRepository roomRepository; + + @Mock + NotificationService notificationService; + + @InjectMocks + BookingSystem bookingSystem; + + @ParameterizedTest + @MethodSource("provideNullScenarios") + void throwsExceptionWhenRoomIdStartTimeOrEndTimeIsNull(String roomId, LocalDateTime startTime, LocalDateTime endTime) { + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.bookRoom(roomId, startTime, endTime)); + assertThat(exception).hasMessage("Bokning kräver giltiga start- och sluttider samt rum-id"); + } + static Stream provideNullScenarios() { + LocalDateTime timeNow = LocalDateTime.now(); + return Stream.of( + Arguments.of(null, timeNow, timeNow), + Arguments.of("Room1", null, timeNow), + Arguments.of("Room1", timeNow, null), + Arguments.of(null, null, null) + ); + } + + @Test + void throwsExceptionWhenRoomDoesNotExist() { + + Room room = new Room("100", "Room1"); + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(2); + LocalDateTime endTime = currentTime.plusDays(3); + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + Mockito.when(roomRepository.findById(room.getId())).thenReturn(Optional.empty()); + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.bookRoom(room.getId(), startTime, endTime)); + assertThat(exception).hasMessage("Rummet existerar inte"); + + } + + @Test + void throwsExceptionWhenStartTimeIsBeforeCurrentDate(){ + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.minusDays(1); + LocalDateTime endTime = currentTime.plusDays(2); + String roomId = "100"; + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.bookRoom(roomId, startTime, endTime)); + assertThat(exception).hasMessage("Kan inte boka tid i dåtid"); + } + + @Test + void throwsExceptionWhenEndTimeIsBeforeStartTime(){ + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(1); + LocalDateTime endTime = currentTime.minusDays(2); + String roomId = "100"; + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.bookRoom(roomId, startTime, endTime)); + assertThat(exception).hasMessage("Sluttid måste vara efter starttid"); + + } + +} From 79aa2e4a1748eeeabfc6d168095366396a3138e7 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 2 Feb 2026 11:11:21 +0100 Subject: [PATCH 02/14] Add tests to check method getAvailableRooms in BookingSystem class --- .../java/com/example/BookingSystemTest.java | 100 +++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java index d5ecde87d..06a189617 100644 --- a/src/test/java/com/example/BookingSystemTest.java +++ b/src/test/java/com/example/BookingSystemTest.java @@ -1,6 +1,5 @@ package com.example; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -12,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -34,14 +34,14 @@ public class BookingSystemTest { BookingSystem bookingSystem; @ParameterizedTest - @MethodSource("provideNullScenarios") + @MethodSource("provideNullScenariosForRoomIdStartAndEndTime") void throwsExceptionWhenRoomIdStartTimeOrEndTimeIsNull(String roomId, LocalDateTime startTime, LocalDateTime endTime) { var exception = assertThrows(IllegalArgumentException.class, () -> bookingSystem.bookRoom(roomId, startTime, endTime)); assertThat(exception).hasMessage("Bokning kräver giltiga start- och sluttider samt rum-id"); } - static Stream provideNullScenarios() { + private static Stream provideNullScenariosForRoomIdStartAndEndTime() { LocalDateTime timeNow = LocalDateTime.now(); return Stream.of( Arguments.of(null, timeNow, timeNow), @@ -99,4 +99,98 @@ void throwsExceptionWhenEndTimeIsBeforeStartTime(){ } + @Test + void bookRoomIfRoomIsAvailable() { + + Room room = new Room("100", "Room1"); + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(2); + LocalDateTime endTime = currentTime.plusDays(3); + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + Mockito.when(roomRepository.findById(room.getId())).thenReturn(Optional.of(room)); + + boolean result = bookingSystem.bookRoom(room.getId(), startTime, endTime); + + assertThat(result).isTrue(); + + Mockito.verify(roomRepository).save(room); + } + + @Test + void notBookRoomIfRoomIsNotAvailable() { + + Room room = new Room("100", "Room1"); + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(1); + LocalDateTime endTime = currentTime.plusDays(2); + Booking booking = new Booking("1", "100", startTime, endTime); + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + Mockito.when(roomRepository.findById(room.getId())).thenReturn(Optional.of(room)); + room.addBooking(booking); + + boolean result = bookingSystem.bookRoom(room.getId(), startTime, endTime); + + assertThat(result).isFalse(); + + Mockito.verify(roomRepository, Mockito.never()).save(room); + + } + + //Test missing + //To check if the notification is sent when a room is booked + + @ParameterizedTest + @MethodSource("provideNullScenarios") + void getAvailableRoomsThrowsExceptionWhenStartOrEndTimeIsNull(LocalDateTime startTime, LocalDateTime endTime) { + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.getAvailableRooms(startTime, endTime)); + assertThat(exception).hasMessage("Måste ange både start- och sluttid"); + } + private static Stream provideNullScenarios() { + LocalDateTime timeNow = LocalDateTime.now(); + return Stream.of( + Arguments.of(timeNow, null), + Arguments.of(null, timeNow), + Arguments.of(null, null) + ); + } + + @Test + void getAvailableRoomsThrowsExceptionWhenEndTimeIsBeforeStartTime() { + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(4); + LocalDateTime endTime = currentTime.plusDays(3); + + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.getAvailableRooms(startTime, endTime)); + assertThat(exception).hasMessage("Sluttid måste vara efter starttid"); + } + + @Test + void getAvailableRoomsReturnsAListOfAvailableRooms() { + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(4); + LocalDateTime endTime = currentTime.plusDays(5); + Room room1 = new Room("101", "Room1"); + Room room2 = new Room("102", "Room2"); + + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room1, room1)); + List result = bookingSystem.getAvailableRooms(startTime, endTime); + + assertThat(result).hasSize(2); + + } + + @Test + void cancelBookingThrowsExceptionWhenBookingIdIsNull() { + + } + + + } From 04eee1dcefbb4f1d7ba47a217ca0bb17681c874e Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Mon, 2 Feb 2026 21:20:15 +0100 Subject: [PATCH 03/14] Add cancelBooking tests. Test coverage 90% --- .../java/com/example/BookingSystemTest.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java index 06a189617..ff2093a6a 100644 --- a/src/test/java/com/example/BookingSystemTest.java +++ b/src/test/java/com/example/BookingSystemTest.java @@ -179,7 +179,7 @@ void getAvailableRoomsReturnsAListOfAvailableRooms() { Room room1 = new Room("101", "Room1"); Room room2 = new Room("102", "Room2"); - Mockito.when(roomRepository.findAll()).thenReturn(List.of(room1, room1)); + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room1, room2)); List result = bookingSystem.getAvailableRooms(startTime, endTime); assertThat(result).hasSize(2); @@ -189,8 +189,46 @@ void getAvailableRoomsReturnsAListOfAvailableRooms() { @Test void cancelBookingThrowsExceptionWhenBookingIdIsNull() { + var exception = assertThrows(IllegalArgumentException.class, + () -> bookingSystem.cancelBooking(null)); + + assertThat(exception).hasMessage("Boknings-id kan inte vara null"); + } + + @Test + void cancelBooking(){ + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(3); + LocalDateTime endTime = currentTime.plusDays(5); + Booking booking = new Booking("1", "100", startTime, endTime); + Room room = new Room("100", "Room1"); + + room.addBooking(booking); + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room)); + boolean result = bookingSystem.cancelBooking("1"); + + assertThat(result).isTrue(); } + @Test + void cancelBookingThrowsExceptionWhenStartTimeIsBeforeCurrentTime(){ + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.minusDays(3); + LocalDateTime endTime = currentTime.plusDays(5); + Booking booking = new Booking("1", "100", startTime, endTime); + Room room = new Room("100", "Room1"); + + room.addBooking(booking); + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room)); + + var exception = assertThrows(IllegalStateException.class, + () -> bookingSystem.cancelBooking("1")); + + assertThat(exception).hasMessage("Kan inte avboka påbörjad eller avslutad bokning"); + } } From 03a2b3fc9e79a127e27b14402c804158a6bab997 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Wed, 4 Feb 2026 21:35:02 +0100 Subject: [PATCH 04/14] Add PITest plugin and improve test robustness. Fix tests to reach now a 96% test strength and 96% mutation coverage in BookingSystem class regarding Pit Test Coverage Report --- pom.xml | 13 ++++ .../java/com/example/BookingSystemTest.java | 60 +++++++++++++------ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index a3aa79e17..c01098191 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,19 @@ + + org.pitest + pitest-maven + 1.22.0 + + + org.pitest + pitest-junit5-plugin + 1.2.2 + + + + org.jacoco jacoco-maven-plugin diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java index ff2093a6a..ebe0711c3 100644 --- a/src/test/java/com/example/BookingSystemTest.java +++ b/src/test/java/com/example/BookingSystemTest.java @@ -5,9 +5,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; @@ -17,9 +15,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -public class BookingSystemTest { +class BookingSystemTest { @Mock TimeProvider timeProvider; @@ -33,6 +33,9 @@ public class BookingSystemTest { @InjectMocks BookingSystem bookingSystem; + @Captor + ArgumentCaptor bookingCaptor; + @ParameterizedTest @MethodSource("provideNullScenariosForRoomIdStartAndEndTime") void throwsExceptionWhenRoomIdStartTimeOrEndTimeIsNull(String roomId, LocalDateTime startTime, LocalDateTime endTime) { @@ -100,7 +103,7 @@ void throwsExceptionWhenEndTimeIsBeforeStartTime(){ } @Test - void bookRoomIfRoomIsAvailable() { + void bookRoomIfRoomIsAvailable() throws NotificationException { Room room = new Room("100", "Room1"); LocalDateTime currentTime = LocalDateTime.now(); @@ -110,11 +113,14 @@ void bookRoomIfRoomIsAvailable() { Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); Mockito.when(roomRepository.findById(room.getId())).thenReturn(Optional.of(room)); - boolean result = bookingSystem.bookRoom(room.getId(), startTime, endTime); + var result = bookingSystem.bookRoom(room.getId(), startTime, endTime); - assertThat(result).isTrue(); + verify(notificationService).sendBookingConfirmation(bookingCaptor.capture()); + Booking bookingCaptorValue = bookingCaptor.getValue(); - Mockito.verify(roomRepository).save(room); + assertThat(room.hasBooking(bookingCaptorValue.getId())).isTrue(); + verify(roomRepository).save(room); + assertThat(result).isTrue(); } @Test @@ -134,13 +140,10 @@ void notBookRoomIfRoomIsNotAvailable() { assertThat(result).isFalse(); - Mockito.verify(roomRepository, Mockito.never()).save(room); + verify(roomRepository, Mockito.never()).save(room); } - //Test missing - //To check if the notification is sent when a room is booked - @ParameterizedTest @MethodSource("provideNullScenarios") void getAvailableRoomsThrowsExceptionWhenStartOrEndTimeIsNull(LocalDateTime startTime, LocalDateTime endTime) { @@ -184,6 +187,7 @@ void getAvailableRoomsReturnsAListOfAvailableRooms() { assertThat(result).hasSize(2); + } @Test @@ -196,20 +200,32 @@ void cancelBookingThrowsExceptionWhenBookingIdIsNull() { } @Test - void cancelBooking(){ + void cancelBooking() throws NotificationException { LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime startTime = currentTime.plusDays(3); LocalDateTime endTime = currentTime.plusDays(5); - Booking booking = new Booking("1", "100", startTime, endTime); - Room room = new Room("100", "Room1"); + Booking booking1 = new Booking("1", "100", startTime, endTime); + Booking booking2 = new Booking("2", "200", startTime, endTime); + Room room1 = new Room("100", "Room1"); + Room room2 = new Room("102", "Room2"); + + room1.addBooking(booking1); + room2.addBooking(booking2); - room.addBooking(booking); Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); - Mockito.when(roomRepository.findAll()).thenReturn(List.of(room)); + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room1, room2)); + boolean result = bookingSystem.cancelBooking("1"); + verify(notificationService).sendCancellationConfirmation(bookingCaptor.capture()); + assertThat(result).isTrue(); + assertThat(room1.hasBooking("1")).isFalse(); + assertThat(room2.hasBooking("2")).isTrue(); + + verify(roomRepository).save(room1); + } @Test @@ -231,4 +247,14 @@ void cancelBookingThrowsExceptionWhenStartTimeIsBeforeCurrentTime(){ assertThat(exception).hasMessage("Kan inte avboka påbörjad eller avslutad bokning"); } + @Test + void cancelBookingShouldReturnFalseIfBookingDoesNotExist() { + Room room = new Room("100", "Room1"); + + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room)); + boolean result = bookingSystem.cancelBooking("999"); + + assertThat(result).isFalse(); + } + } From c04e9dcf1a96b6c1c65aea1d698df79b3ac41627 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Wed, 4 Feb 2026 21:48:32 +0100 Subject: [PATCH 05/14] Add a short description in every test --- .../java/com/example/BookingSystemTest.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java index ebe0711c3..e30ccc29f 100644 --- a/src/test/java/com/example/BookingSystemTest.java +++ b/src/test/java/com/example/BookingSystemTest.java @@ -1,5 +1,6 @@ package com.example; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -15,7 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -38,6 +38,7 @@ class BookingSystemTest { @ParameterizedTest @MethodSource("provideNullScenariosForRoomIdStartAndEndTime") + @DisplayName("Should throw IllegalArgumentException if room ID, start time, or end time is null") void throwsExceptionWhenRoomIdStartTimeOrEndTimeIsNull(String roomId, LocalDateTime startTime, LocalDateTime endTime) { var exception = assertThrows(IllegalArgumentException.class, @@ -55,6 +56,7 @@ private static Stream provideNullScenariosForRoomIdStartAndEndTime() } @Test + @DisplayName("Should throw IllegalArgumentException if the room ID provided does not exist in the repository") void throwsExceptionWhenRoomDoesNotExist() { Room room = new Room("100", "Room1"); @@ -68,10 +70,10 @@ void throwsExceptionWhenRoomDoesNotExist() { var exception = assertThrows(IllegalArgumentException.class, () -> bookingSystem.bookRoom(room.getId(), startTime, endTime)); assertThat(exception).hasMessage("Rummet existerar inte"); - } @Test + @DisplayName("Should throw IllegalArgumentException when attempting to book a room in the past") void throwsExceptionWhenStartTimeIsBeforeCurrentDate(){ LocalDateTime currentTime = LocalDateTime.now(); @@ -87,6 +89,7 @@ void throwsExceptionWhenStartTimeIsBeforeCurrentDate(){ } @Test + @DisplayName("Should throw IllegalArgumentException if the booking end time is before the start time") void throwsExceptionWhenEndTimeIsBeforeStartTime(){ LocalDateTime currentTime = LocalDateTime.now(); @@ -99,10 +102,10 @@ void throwsExceptionWhenEndTimeIsBeforeStartTime(){ var exception = assertThrows(IllegalArgumentException.class, () -> bookingSystem.bookRoom(roomId, startTime, endTime)); assertThat(exception).hasMessage("Sluttid måste vara efter starttid"); - } @Test + @DisplayName("Should successfully book a room, save it to the repository, and send a confirmation when the room is available") void bookRoomIfRoomIsAvailable() throws NotificationException { Room room = new Room("100", "Room1"); @@ -124,6 +127,7 @@ void bookRoomIfRoomIsAvailable() throws NotificationException { } @Test + @DisplayName("Should return false and not save to repository if the room is already occupied during the requested time") void notBookRoomIfRoomIsNotAvailable() { Room room = new Room("100", "Room1"); @@ -141,11 +145,11 @@ void notBookRoomIfRoomIsNotAvailable() { assertThat(result).isFalse(); verify(roomRepository, Mockito.never()).save(room); - } @ParameterizedTest @MethodSource("provideNullScenarios") + @DisplayName("Should throw IllegalArgumentException when searching for available rooms with null dates") void getAvailableRoomsThrowsExceptionWhenStartOrEndTimeIsNull(LocalDateTime startTime, LocalDateTime endTime) { var exception = assertThrows(IllegalArgumentException.class, @@ -162,6 +166,7 @@ private static Stream provideNullScenarios() { } @Test + @DisplayName("Should throw IllegalArgumentException if the end time is before the start time during availability search") void getAvailableRoomsThrowsExceptionWhenEndTimeIsBeforeStartTime() { LocalDateTime currentTime = LocalDateTime.now(); @@ -174,6 +179,7 @@ void getAvailableRoomsThrowsExceptionWhenEndTimeIsBeforeStartTime() { } @Test + @DisplayName("Should return a list of all rooms that do not have overlapping bookings for the given period") void getAvailableRoomsReturnsAListOfAvailableRooms() { LocalDateTime currentTime = LocalDateTime.now(); @@ -186,11 +192,10 @@ void getAvailableRoomsReturnsAListOfAvailableRooms() { List result = bookingSystem.getAvailableRooms(startTime, endTime); assertThat(result).hasSize(2); - - } @Test + @DisplayName("Should throw IllegalArgumentException if the booking ID to cancel is null") void cancelBookingThrowsExceptionWhenBookingIdIsNull() { var exception = assertThrows(IllegalArgumentException.class, @@ -200,6 +205,7 @@ void cancelBookingThrowsExceptionWhenBookingIdIsNull() { } @Test + @DisplayName("Should successfully cancel a booking, update the correct room, and send a cancellation notification") void cancelBooking() throws NotificationException { LocalDateTime currentTime = LocalDateTime.now(); @@ -225,10 +231,10 @@ void cancelBooking() throws NotificationException { assertThat(room2.hasBooking("2")).isTrue(); verify(roomRepository).save(room1); - } @Test + @DisplayName("Should throw IllegalStateException when trying to cancel a booking that has already started or finished") void cancelBookingThrowsExceptionWhenStartTimeIsBeforeCurrentTime(){ LocalDateTime currentTime = LocalDateTime.now(); @@ -248,6 +254,7 @@ void cancelBookingThrowsExceptionWhenStartTimeIsBeforeCurrentTime(){ } @Test + @DisplayName("Should return false when attempting to cancel a booking ID that is not found in any room") void cancelBookingShouldReturnFalseIfBookingDoesNotExist() { Room room = new Room("100", "Room1"); From 27ecbfc507cbb7b63565979af367b2a5aab81d26 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Wed, 4 Feb 2026 22:02:00 +0100 Subject: [PATCH 06/14] Sync pom.xml from uppgift1 to include Pitest and test dependencies. --- pom.xml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 543c1aa50..c01098191 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ org.junit.jupiter junit-jupiter - 5.11.4 + 6.0.1 test @@ -28,9 +28,14 @@ org.mockito mockito-junit-jupiter - 5.15.2 + 5.21.0 test + + org.awaitility + awaitility + 4.3.0 + @@ -57,6 +62,19 @@ + + org.pitest + pitest-maven + 1.22.0 + + + org.pitest + pitest-junit5-plugin + 1.2.2 + + + + org.jacoco jacoco-maven-plugin From 7d9f5f5f8944befed2da7b9141eef97cd8b32677 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Thu, 5 Feb 2026 17:37:12 +0100 Subject: [PATCH 07/14] Add ShoppingCart and Item with initial addItem test --- src/main/java/com/example/shop/Item.java | 17 ++++++++++++++++ .../java/com/example/shop/ShoppingCart.java | 18 +++++++++++++++++ .../com/example/shop/ShoppingCartTest.java | 20 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/main/java/com/example/shop/Item.java create mode 100644 src/main/java/com/example/shop/ShoppingCart.java create mode 100644 src/test/java/com/example/shop/ShoppingCartTest.java diff --git a/src/main/java/com/example/shop/Item.java b/src/main/java/com/example/shop/Item.java new file mode 100644 index 000000000..8656f3017 --- /dev/null +++ b/src/main/java/com/example/shop/Item.java @@ -0,0 +1,17 @@ +package com.example.shop; + +public class Item { + + private String id; + private double price; + private int quantity; + + public Item(String id, double price, int quantity) { + this.id = id; + this.price = price; + this.quantity = quantity; + } + + + +} diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java new file mode 100644 index 000000000..ef202b480 --- /dev/null +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -0,0 +1,18 @@ +package com.example.shop; + +import java.util.ArrayList; +import java.util.List; + +public class ShoppingCart { + + private List items = new ArrayList<>(); + + public void addItem(Item item){ + this.items.add(item); + } + public List getItems() { + return items; + } + + +} diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java new file mode 100644 index 000000000..afd181f6e --- /dev/null +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -0,0 +1,20 @@ +package com.example.shop; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ShoppingCartTest { + + @Test + @DisplayName("Should increase the number of items in the cart when adding products") + void addItemToShoppingCart(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + + cart.addItem(shirt); + + assertThat(cart.getItems()).hasSize(1); + } +} From d5905fb6da0408065d5272306e598bd84722a5f8 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Thu, 5 Feb 2026 17:47:21 +0100 Subject: [PATCH 08/14] Add removeItemInShoppingCart test --- src/main/java/com/example/shop/ShoppingCart.java | 3 +++ .../java/com/example/shop/ShoppingCartTest.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java index ef202b480..4684e190e 100644 --- a/src/main/java/com/example/shop/ShoppingCart.java +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -13,6 +13,9 @@ public void addItem(Item item){ public List getItems() { return items; } + public void removeItem(Item item){ + this.items.remove(item); + } } diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java index afd181f6e..3be616d5f 100644 --- a/src/test/java/com/example/shop/ShoppingCartTest.java +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -17,4 +17,18 @@ void addItemToShoppingCart(){ assertThat(cart.getItems()).hasSize(1); } + + @Test + @DisplayName("Should decrease the number of items in the cart when removing products") + void removeItemInShoppingCart(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + Item shirt2 = new Item("2", 200.0, 1); + + cart.addItem(shirt); + cart.addItem(shirt2); + cart.removeItem(shirt2); + + assertThat(cart.getItems()).hasSize(1); + } } From 67a84fbe1ccfe886e8f0b80187f9f74c5ffea2e8 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Thu, 5 Feb 2026 18:42:45 +0100 Subject: [PATCH 09/14] Add getTotalPriceOfShoppingCart test --- src/main/java/com/example/shop/Item.java | 6 ++++++ src/main/java/com/example/shop/ShoppingCart.java | 8 ++++++++ .../java/com/example/shop/ShoppingCartTest.java | 15 +++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/main/java/com/example/shop/Item.java b/src/main/java/com/example/shop/Item.java index 8656f3017..98f5dcf4c 100644 --- a/src/main/java/com/example/shop/Item.java +++ b/src/main/java/com/example/shop/Item.java @@ -11,6 +11,12 @@ public Item(String id, double price, int quantity) { this.price = price; this.quantity = quantity; } + public double getPrice() { + return price; + } + public int getQuantity() { + return quantity; + } diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java index 4684e190e..c982dc1eb 100644 --- a/src/main/java/com/example/shop/ShoppingCart.java +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -6,6 +6,7 @@ public class ShoppingCart { private List items = new ArrayList<>(); + private double totalPrice; public void addItem(Item item){ this.items.add(item); @@ -16,6 +17,13 @@ public List getItems() { public void removeItem(Item item){ this.items.remove(item); } + public double getTotalPrice(){ + return items.stream() + .mapToDouble(item -> item.getPrice() * item.getQuantity()) + .sum(); + + } + } diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java index 3be616d5f..19b48e447 100644 --- a/src/test/java/com/example/shop/ShoppingCartTest.java +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -31,4 +31,19 @@ void removeItemInShoppingCart(){ assertThat(cart.getItems()).hasSize(1); } + + @Test + @DisplayName("Should calculate the total price of the shopping cart") + void getTotalPriceOfShoppingCart(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 2); + Item pants = new Item("2", 200.0, 1); + Item hat = new Item("3", 300.0, 2); + + cart.addItem(shirt); + cart.addItem(pants); + cart.addItem(hat); + + assertThat(cart.getTotalPrice()).isEqualTo(1000.0); + } } From a51a0f7d4054f05326df6b29c75c488443dfa99a Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Thu, 5 Feb 2026 19:32:43 +0100 Subject: [PATCH 10/14] Add discount functionality to ShoppingCart. Add test applyDiscountToShoppingCart --- src/main/java/com/example/shop/Discount.java | 12 ++++++++++++ .../java/com/example/shop/DiscountProvider.java | 5 +++++ src/main/java/com/example/shop/ShoppingCart.java | 15 +++++++++++++-- .../java/com/example/shop/ShoppingCartTest.java | 16 ++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/shop/Discount.java create mode 100644 src/main/java/com/example/shop/DiscountProvider.java diff --git a/src/main/java/com/example/shop/Discount.java b/src/main/java/com/example/shop/Discount.java new file mode 100644 index 000000000..ba34ecbbe --- /dev/null +++ b/src/main/java/com/example/shop/Discount.java @@ -0,0 +1,12 @@ +package com.example.shop; + +public class Discount { + private int percentage; + + public Discount(int percentage) { + this.percentage = percentage; + } + public double applyDiscount(double totalPrice){ + return totalPrice - (totalPrice * ((double) this.percentage /100)); + } +} diff --git a/src/main/java/com/example/shop/DiscountProvider.java b/src/main/java/com/example/shop/DiscountProvider.java new file mode 100644 index 000000000..9b631d67a --- /dev/null +++ b/src/main/java/com/example/shop/DiscountProvider.java @@ -0,0 +1,5 @@ +package com.example.shop; + +public interface DiscountProvider { + double applyDiscount(double totalPrice); +} diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java index c982dc1eb..ab518f2a4 100644 --- a/src/main/java/com/example/shop/ShoppingCart.java +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -6,7 +6,7 @@ public class ShoppingCart { private List items = new ArrayList<>(); - private double totalPrice; + private Discount discount; public void addItem(Item item){ this.items.add(item); @@ -17,11 +17,22 @@ public List getItems() { public void removeItem(Item item){ this.items.remove(item); } + + public void setDiscount(Discount discount) { + this.discount = discount; + } + public double getTotalPrice(){ - return items.stream() + double totalPrice = items.stream() .mapToDouble(item -> item.getPrice() * item.getQuantity()) .sum(); + if (discount != null) { + return discount.applyDiscount(totalPrice); + } + + return totalPrice; + } diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java index 19b48e447..3a52338e3 100644 --- a/src/test/java/com/example/shop/ShoppingCartTest.java +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -46,4 +46,20 @@ void getTotalPriceOfShoppingCart(){ assertThat(cart.getTotalPrice()).isEqualTo(1000.0); } + + @Test + @DisplayName("Should apply discount to the shopping cart") + void applyDiscountToShoppingCart(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + Item pants = new Item("2", 200.0, 1); + Discount discount = new Discount(50); + + cart.addItem(shirt); + cart.addItem(pants); + cart.setDiscount(discount); + + assertThat(cart.getTotalPrice()).isEqualTo(150); + + } } From dba4112d169bee1d64803fec45dcdb3ace165b9f Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Thu, 5 Feb 2026 21:37:54 +0100 Subject: [PATCH 11/14] Update ShoppingCart to merge quantities for existing items when new items are added. Add test for manageQuantityUpdate --- src/main/java/com/example/shop/Item.java | 8 ++++++-- src/main/java/com/example/shop/ShoppingCart.java | 14 ++++++++++++-- .../java/com/example/shop/ShoppingCartTest.java | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/shop/Item.java b/src/main/java/com/example/shop/Item.java index 98f5dcf4c..438bd768a 100644 --- a/src/main/java/com/example/shop/Item.java +++ b/src/main/java/com/example/shop/Item.java @@ -17,7 +17,11 @@ public double getPrice() { public int getQuantity() { return quantity; } - - + public void setQuantity(int quantity) { + this.quantity = quantity; + } + public String getId() { + return id; + } } diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java index ab518f2a4..1977e843c 100644 --- a/src/main/java/com/example/shop/ShoppingCart.java +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -8,8 +8,18 @@ public class ShoppingCart { private List items = new ArrayList<>(); private Discount discount; - public void addItem(Item item){ - this.items.add(item); + public void addItem(Item newItem){ + + for(Item existingItem : items){ + if (existingItem.getId().equals(newItem.getId())) { + int updatedQuantity = existingItem.getQuantity() + newItem.getQuantity(); + existingItem.setQuantity(updatedQuantity); + + return; + } + } + + this.items.add(newItem); } public List getItems() { return items; diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java index 3a52338e3..88aebabdd 100644 --- a/src/test/java/com/example/shop/ShoppingCartTest.java +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -62,4 +62,20 @@ void applyDiscountToShoppingCart(){ assertThat(cart.getTotalPrice()).isEqualTo(150); } + + @Test + @DisplayName("Should update the quantity of an item when new item added in the cart is the same as the existing") + void manageQuantityUpdate(){ + ShoppingCart cart = new ShoppingCart(); + Item item1 = new Item("A1", 100.0, 1); + Item item2 = new Item("A1", 100.0, 2); + + cart.addItem(item1); + cart.addItem(item2); + + int totalQuantity = cart.getItems().getFirst().getQuantity(); + + assertThat(cart.getItems()).hasSize(1); + assertThat(totalQuantity).isEqualTo(3); + } } From 7f5ad4b96c4f0bfa2a6a87610f481b3e7701e4b1 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Fri, 6 Feb 2026 11:10:54 +0100 Subject: [PATCH 12/14] Add edge cases tests for quantity and discounts --- src/main/java/com/example/shop/Discount.java | 8 +- .../java/com/example/shop/ShoppingCart.java | 21 ++--- .../com/example/shop/ShoppingCartTest.java | 79 +++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/shop/Discount.java b/src/main/java/com/example/shop/Discount.java index ba34ecbbe..5464e4d69 100644 --- a/src/main/java/com/example/shop/Discount.java +++ b/src/main/java/com/example/shop/Discount.java @@ -6,7 +6,13 @@ public class Discount { public Discount(int percentage) { this.percentage = percentage; } + public double applyDiscount(double totalPrice){ - return totalPrice - (totalPrice * ((double) this.percentage /100)); + double result = totalPrice - (totalPrice * ((double) this.percentage /100)); + return Math.max(0, result); + } + + public int getPercentage() { + return percentage; } } diff --git a/src/main/java/com/example/shop/ShoppingCart.java b/src/main/java/com/example/shop/ShoppingCart.java index 1977e843c..0080b6956 100644 --- a/src/main/java/com/example/shop/ShoppingCart.java +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -10,20 +10,23 @@ public class ShoppingCart { public void addItem(Item newItem){ - for(Item existingItem : items){ - if (existingItem.getId().equals(newItem.getId())) { - int updatedQuantity = existingItem.getQuantity() + newItem.getQuantity(); - existingItem.setQuantity(updatedQuantity); - - return; + if ( newItem.getQuantity() > 0) { + for (Item existingItem : items) { + if (existingItem.getId().equals(newItem.getId())) { + int updatedQuantity = existingItem.getQuantity() + newItem.getQuantity(); + existingItem.setQuantity(updatedQuantity); + + return; + } } + this.items.add(newItem); } - - this.items.add(newItem); } + public List getItems() { return items; } + public void removeItem(Item item){ this.items.remove(item); } @@ -37,7 +40,7 @@ public double getTotalPrice(){ .mapToDouble(item -> item.getPrice() * item.getQuantity()) .sum(); - if (discount != null) { + if (discount != null && discount.getPercentage() <= 100 && discount.getPercentage() > 0) { return discount.applyDiscount(totalPrice); } diff --git a/src/test/java/com/example/shop/ShoppingCartTest.java b/src/test/java/com/example/shop/ShoppingCartTest.java index 88aebabdd..e5b4f11ca 100644 --- a/src/test/java/com/example/shop/ShoppingCartTest.java +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -78,4 +78,83 @@ void manageQuantityUpdate(){ assertThat(cart.getItems()).hasSize(1); assertThat(totalQuantity).isEqualTo(3); } + + @Test + @DisplayName("Should return total price 0 when shopping cart is empty") + void totalPriceShouldBeZeroWhenShoppingCartIsEmpty(){ + ShoppingCart cart = new ShoppingCart(); + + cart.getTotalPrice(); + + assertThat(cart.getItems()).isEmpty(); + assertThat(cart.getTotalPrice()).isEqualTo(0.0); + } + + @Test + @DisplayName("Should not colapse when removing an item that is not in the cart") + void removeItemThatDoesNotExist(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + + cart.removeItem(shirt); + + assertThat(cart.getItems()).isEmpty(); + } + + @Test + @DisplayName("Should return same total price when discount is more than 100%") + void discountShouldNotBeMoreThan100(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 1000.0, 1); + Discount discount = new Discount(110); + + cart.addItem(shirt); + cart.setDiscount(discount); + + assertThat(cart.getTotalPrice()).isEqualTo(1000.0); + } + + @Test + @DisplayName("Should return same total price when discount is a negative number") + void discountShouldNotBeNegative(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + Discount discount = new Discount(-50); + + cart.addItem(shirt); + cart.setDiscount(discount); + + assertThat(cart.getTotalPrice()).isEqualTo(100.0); + } + + @Test + @DisplayName("Should apply discount even when adding more items to the cart after the first item") + void shouldApplyDiscountWhenAddingMoreItemsAfter(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, 1); + Item pants = new Item("2", 200.0, 1); + Item hat = new Item("3", 300.0, 2); + Discount discount = new Discount(50); + + cart.addItem(shirt); + cart.setDiscount(discount); + cart.addItem(pants); + cart.addItem(hat); + + assertThat(cart.getTotalPrice()).isEqualTo(450.0); + } + + @Test + @DisplayName("Should not add any item when the quantity is negative") + void notAddItemsWhenQuantityOfItemsIsNegative(){ + ShoppingCart cart = new ShoppingCart(); + Item shirt = new Item("1", 100.0, -1); + Item pants = new Item("2", 200.0, -1); + + cart.addItem(shirt); + cart.addItem(pants); + + assertThat(cart.getItems()).isEmpty(); + } + } From 33a5cdefba8d718c05e0238647bd21e32ae1c00e Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Fri, 6 Feb 2026 18:58:55 +0100 Subject: [PATCH 13/14] Apply dependency injection to PaymentProcessor for testability --- pom.xml | 22 +++++++- .../example/payment/DatabaseConnection.java | 7 +++ .../com/example/payment/EmailService.java | 5 ++ .../java/com/example/payment/PaymentApi.java | 5 ++ .../example/payment/PaymentApiResponse.java | 4 ++ .../com/example/payment/PaymentProcessor.java | 53 +++++++++++-------- 6 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/example/payment/DatabaseConnection.java create mode 100644 src/main/java/com/example/payment/EmailService.java create mode 100644 src/main/java/com/example/payment/PaymentApi.java create mode 100644 src/main/java/com/example/payment/PaymentApiResponse.java diff --git a/pom.xml b/pom.xml index 543c1aa50..c01098191 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ org.junit.jupiter junit-jupiter - 5.11.4 + 6.0.1 test @@ -28,9 +28,14 @@ org.mockito mockito-junit-jupiter - 5.15.2 + 5.21.0 test + + org.awaitility + awaitility + 4.3.0 + @@ -57,6 +62,19 @@ + + org.pitest + pitest-maven + 1.22.0 + + + org.pitest + pitest-junit5-plugin + 1.2.2 + + + + org.jacoco jacoco-maven-plugin diff --git a/src/main/java/com/example/payment/DatabaseConnection.java b/src/main/java/com/example/payment/DatabaseConnection.java new file mode 100644 index 000000000..a295229e2 --- /dev/null +++ b/src/main/java/com/example/payment/DatabaseConnection.java @@ -0,0 +1,7 @@ +package com.example.payment; + +import java.sql.PreparedStatement; + +public interface DatabaseConnection { + PreparedStatement getInstance(); +} diff --git a/src/main/java/com/example/payment/EmailService.java b/src/main/java/com/example/payment/EmailService.java new file mode 100644 index 000000000..e748c2fa2 --- /dev/null +++ b/src/main/java/com/example/payment/EmailService.java @@ -0,0 +1,5 @@ +package com.example.payment; + +public interface EmailService { + void sendPaymentConfirmation(String email, double amount); +} diff --git a/src/main/java/com/example/payment/PaymentApi.java b/src/main/java/com/example/payment/PaymentApi.java new file mode 100644 index 000000000..5f741d58c --- /dev/null +++ b/src/main/java/com/example/payment/PaymentApi.java @@ -0,0 +1,5 @@ +package com.example.payment; + +public interface PaymentApi { + PaymentApiResponse charge(String apiKey, double amount); +} diff --git a/src/main/java/com/example/payment/PaymentApiResponse.java b/src/main/java/com/example/payment/PaymentApiResponse.java new file mode 100644 index 000000000..208d2b111 --- /dev/null +++ b/src/main/java/com/example/payment/PaymentApiResponse.java @@ -0,0 +1,4 @@ +package com.example.payment; + +public record PaymentApiResponse(boolean success) { +} diff --git a/src/main/java/com/example/payment/PaymentProcessor.java b/src/main/java/com/example/payment/PaymentProcessor.java index 137bab7d9..d1e335b85 100644 --- a/src/main/java/com/example/payment/PaymentProcessor.java +++ b/src/main/java/com/example/payment/PaymentProcessor.java @@ -1,23 +1,34 @@ package com.example.payment; -//public class PaymentProcessor { -// private static final String API_KEY = "sk_test_123456"; -// -// public boolean processPayment(double amount) { -// // Anropar extern betaltjänst direkt med statisk API-nyckel -// PaymentApiResponse response = PaymentApi.charge(API_KEY, amount); -// -// // Skriver till databas direkt -// if (response.isSuccess()) { -// DatabaseConnection.getInstance() -// .executeUpdate("INSERT INTO payments (amount, status) VALUES (" + amount + ", 'SUCCESS')"); -// } -// -// // Skickar e-post direkt -// if (response.isSuccess()) { -// EmailService.sendPaymentConfirmation("user@example.com", amount); -// } -// -// return response.isSuccess(); -// } -//} +import java.sql.SQLException; + +public class PaymentProcessor { + private static final String API_KEY = "sk_test_123456"; + private final DatabaseConnection databaseConnection; + private final EmailService emailService; + private final PaymentApi paymentApi; + + public PaymentProcessor(DatabaseConnection databaseConnection, EmailService emailService, PaymentApi paymentApi) { + this.databaseConnection = databaseConnection; + this.emailService = emailService; + this.paymentApi = paymentApi; + } + + public boolean processPayment(double amount) throws SQLException { + // Anropar extern betaltjänst direkt med statisk API-nyckel + PaymentApiResponse response = paymentApi.charge(API_KEY, amount); + + // Skriver till databas direkt + if (response.success()) { + databaseConnection.getInstance() + .executeUpdate("INSERT INTO payments (amount, status) VALUES (" + amount + ", 'SUCCESS')"); + } + + // Skickar e-post direkt + if (response.success()) { + emailService.sendPaymentConfirmation("user@example.com", amount); + } + + return response.success(); + } +} From 361545b06541e800a89bc61769d431975063a206 Mon Sep 17 00:00:00 2001 From: Gabriela Aguirre Date: Fri, 6 Feb 2026 20:25:31 +0100 Subject: [PATCH 14/14] Refactor DatabaseConnection and add PaymentProcessorTest --- .../example/payment/DatabaseConnection.java | 4 +- .../com/example/payment/PaymentProcessor.java | 8 +- .../com/example/PaymentProcessorTest.java | 77 +++++++++++++++++++ 3 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/example/PaymentProcessorTest.java diff --git a/src/main/java/com/example/payment/DatabaseConnection.java b/src/main/java/com/example/payment/DatabaseConnection.java index a295229e2..22b856e38 100644 --- a/src/main/java/com/example/payment/DatabaseConnection.java +++ b/src/main/java/com/example/payment/DatabaseConnection.java @@ -1,7 +1,5 @@ package com.example.payment; -import java.sql.PreparedStatement; - public interface DatabaseConnection { - PreparedStatement getInstance(); + void executeUpdate(String query); } diff --git a/src/main/java/com/example/payment/PaymentProcessor.java b/src/main/java/com/example/payment/PaymentProcessor.java index d1e335b85..de22a6562 100644 --- a/src/main/java/com/example/payment/PaymentProcessor.java +++ b/src/main/java/com/example/payment/PaymentProcessor.java @@ -1,7 +1,5 @@ package com.example.payment; -import java.sql.SQLException; - public class PaymentProcessor { private static final String API_KEY = "sk_test_123456"; private final DatabaseConnection databaseConnection; @@ -14,13 +12,13 @@ public PaymentProcessor(DatabaseConnection databaseConnection, EmailService emai this.paymentApi = paymentApi; } - public boolean processPayment(double amount) throws SQLException { + public boolean processPayment(double amount) { // Anropar extern betaltjänst direkt med statisk API-nyckel - PaymentApiResponse response = paymentApi.charge(API_KEY, amount); + PaymentApiResponse response = this.paymentApi.charge(API_KEY, amount); // Skriver till databas direkt if (response.success()) { - databaseConnection.getInstance() + this.databaseConnection .executeUpdate("INSERT INTO payments (amount, status) VALUES (" + amount + ", 'SUCCESS')"); } diff --git a/src/test/java/com/example/PaymentProcessorTest.java b/src/test/java/com/example/PaymentProcessorTest.java new file mode 100644 index 000000000..f81bfbeac --- /dev/null +++ b/src/test/java/com/example/PaymentProcessorTest.java @@ -0,0 +1,77 @@ +package com.example; + +import com.example.payment.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PaymentProcessorTest { + + @Mock + private PaymentApi paymentApi; + + @Mock + private DatabaseConnection databaseConnection; + + @Mock + private EmailService emailService; + + @InjectMocks + private PaymentProcessor paymentProcessor; + + @Test + @DisplayName("Should return true and save to database when payment is successful") + void paymentSuccess() throws SQLException { + PaymentApiResponse response = new PaymentApiResponse(true); + double amount = 100.0; + + Mockito.when(paymentApi.charge("sk_test_123456", amount)).thenReturn(response); + boolean result = paymentProcessor.processPayment(amount); + + assertThat(result).isTrue(); + verify(databaseConnection).executeUpdate(anyString()); + } + + @Test + @DisplayName("Should send email notification with correct details on success") + void sendEmailNotificationWhenPaymentIsSuccessful() throws SQLException { + PaymentApiResponse response = new PaymentApiResponse(true); + double amount = 100.0; + String email = "user@example.com"; + + Mockito.when(paymentApi.charge("sk_test_123456", amount)).thenReturn(response); + boolean result = paymentProcessor.processPayment(amount); + + assertThat(result).isTrue(); + verify(databaseConnection).executeUpdate(anyString()); + verify(emailService).sendPaymentConfirmation(email, amount); + } + + @Test + @DisplayName("Should return false and perform no side effects when payment fails") + void paymentFails() throws SQLException { + PaymentApiResponse response = new PaymentApiResponse(false); + double amount = 100.0; + + Mockito.when(paymentApi.charge("sk_test_123456", amount)).thenReturn(response); + boolean result = paymentProcessor.processPayment(amount); + + assertThat(result).isFalse(); + verify(databaseConnection, never()).executeUpdate(anyString()); + verify(emailService, never()).sendPaymentConfirmation(anyString(), anyDouble()); + } + +}