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..22b856e38 --- /dev/null +++ b/src/main/java/com/example/payment/DatabaseConnection.java @@ -0,0 +1,5 @@ +package com.example.payment; + +public interface DatabaseConnection { + void executeUpdate(String query); +} 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..de22a6562 100644 --- a/src/main/java/com/example/payment/PaymentProcessor.java +++ b/src/main/java/com/example/payment/PaymentProcessor.java @@ -1,23 +1,32 @@ 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(); -// } -//} +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) { + // Anropar extern betaltjänst direkt med statisk API-nyckel + PaymentApiResponse response = this.paymentApi.charge(API_KEY, amount); + + // Skriver till databas direkt + if (response.success()) { + this.databaseConnection + .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(); + } +} 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..5464e4d69 --- /dev/null +++ b/src/main/java/com/example/shop/Discount.java @@ -0,0 +1,18 @@ +package com.example.shop; + +public class Discount { + private int percentage; + + public Discount(int percentage) { + this.percentage = percentage; + } + + public double applyDiscount(double totalPrice){ + 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/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/Item.java b/src/main/java/com/example/shop/Item.java new file mode 100644 index 000000000..438bd768a --- /dev/null +++ b/src/main/java/com/example/shop/Item.java @@ -0,0 +1,27 @@ +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; + } + public double getPrice() { + return price; + } + 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 new file mode 100644 index 000000000..0080b6956 --- /dev/null +++ b/src/main/java/com/example/shop/ShoppingCart.java @@ -0,0 +1,53 @@ +package com.example.shop; + +import java.util.ArrayList; +import java.util.List; + +public class ShoppingCart { + + private List items = new ArrayList<>(); + private Discount discount; + + public void addItem(Item newItem){ + + 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); + } + } + + public List getItems() { + return items; + } + + public void removeItem(Item item){ + this.items.remove(item); + } + + public void setDiscount(Discount discount) { + this.discount = discount; + } + + public double getTotalPrice(){ + double totalPrice = items.stream() + .mapToDouble(item -> item.getPrice() * item.getQuantity()) + .sum(); + + if (discount != null && discount.getPercentage() <= 100 && discount.getPercentage() > 0) { + return discount.applyDiscount(totalPrice); + } + + return totalPrice; + + } + + + +} diff --git a/src/test/java/com/example/BookingSystemTest.java b/src/test/java/com/example/BookingSystemTest.java new file mode 100644 index 000000000..e30ccc29f --- /dev/null +++ b/src/test/java/com/example/BookingSystemTest.java @@ -0,0 +1,267 @@ +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; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +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; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class BookingSystemTest { + + @Mock + TimeProvider timeProvider; + + @Mock + RoomRepository roomRepository; + + @Mock + NotificationService notificationService; + + @InjectMocks + BookingSystem bookingSystem; + + @Captor + ArgumentCaptor bookingCaptor; + + @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, + () -> bookingSystem.bookRoom(roomId, startTime, endTime)); + assertThat(exception).hasMessage("Bokning kräver giltiga start- och sluttider samt rum-id"); + } + private static Stream provideNullScenariosForRoomIdStartAndEndTime() { + 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 + @DisplayName("Should throw IllegalArgumentException if the room ID provided does not exist in the repository") + 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 + @DisplayName("Should throw IllegalArgumentException when attempting to book a room in the past") + 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 + @DisplayName("Should throw IllegalArgumentException if the booking end time is before the start time") + 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"); + } + + @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"); + 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)); + + var result = bookingSystem.bookRoom(room.getId(), startTime, endTime); + + verify(notificationService).sendBookingConfirmation(bookingCaptor.capture()); + Booking bookingCaptorValue = bookingCaptor.getValue(); + + assertThat(room.hasBooking(bookingCaptorValue.getId())).isTrue(); + verify(roomRepository).save(room); + assertThat(result).isTrue(); + } + + @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"); + 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(); + + 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, + () -> 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 + @DisplayName("Should throw IllegalArgumentException if the end time is before the start time during availability search") + 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 + @DisplayName("Should return a list of all rooms that do not have overlapping bookings for the given period") + 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, room2)); + 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, + () -> bookingSystem.cancelBooking(null)); + + assertThat(exception).hasMessage("Boknings-id kan inte vara null"); + } + + @Test + @DisplayName("Should successfully cancel a booking, update the correct room, and send a cancellation notification") + void cancelBooking() throws NotificationException { + + LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime startTime = currentTime.plusDays(3); + LocalDateTime endTime = currentTime.plusDays(5); + 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); + + Mockito.when(timeProvider.getCurrentTime()).thenReturn(currentTime); + 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 + @DisplayName("Should throw IllegalStateException when trying to cancel a booking that has already started or finished") + 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"); + } + + @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"); + + Mockito.when(roomRepository.findAll()).thenReturn(List.of(room)); + boolean result = bookingSystem.cancelBooking("999"); + + assertThat(result).isFalse(); + } + +} 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()); + } + +} 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..e5b4f11ca --- /dev/null +++ b/src/test/java/com/example/shop/ShoppingCartTest.java @@ -0,0 +1,160 @@ +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); + } + + @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); + } + + @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); + } + + @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); + + } + + @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); + } + + @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(); + } + +}