From 789245c7063f5247504e0932c7a69c7f530bb8d6 Mon Sep 17 00:00:00 2001 From: "Popov, Kristian" Date: Wed, 19 Aug 2026 13:38:06 +0200 Subject: [PATCH 1/2] fix: refuse borrowing against another member's membership --- .../adapters/input/rest/TransactionController.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java index 67033bf..7b2de24 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java @@ -93,12 +93,21 @@ public ResponseEntity returnBook(@PathVariable UUID bookId, } } + /** Only the member themselves or an administrator may borrow against a membership. */ @PostMapping(value = "/borrowBook/{customerId}/{bookId}", produces = {"application/transaction-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Borrow a book") public ResponseEntity borrowBook( @PathVariable UUID customerId, - @PathVariable UUID bookId) { + @PathVariable UUID bookId, + Authentication authentication) { + // The customer id comes from the path, so without this a member could borrow against + // somebody else's membership and spend their loan limit. Matches returnBook and extendLoan. + if (!isAdmin(authentication) && !isOwner(authentication, customerId)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body("You can only borrow against your own membership."); + } + try { transactionUseCase.borrowBook(customerId, bookId); return ResponseEntity.ok("Book borrowed successfully."); From 67ebc502086afa13b2e6a3ac3b6cb3fbdafbd764 Mon Sep 17 00:00:00 2001 From: "Popov, Kristian" Date: Wed, 19 Aug 2026 13:38:07 +0200 Subject: [PATCH 2/2] test: cover borrowing against another membership, and borrow as the owner --- .../input/TransactionControllerTestIT.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Library-Management-System-Version-2/src/test/java/app/adapters/input/TransactionControllerTestIT.java b/Library-Management-System-Version-2/src/test/java/app/adapters/input/TransactionControllerTestIT.java index b6e7a6d..9361087 100644 --- a/Library-Management-System-Version-2/src/test/java/app/adapters/input/TransactionControllerTestIT.java +++ b/Library-Management-System-Version-2/src/test/java/app/adapters/input/TransactionControllerTestIT.java @@ -125,6 +125,7 @@ void testCreateNewTransaction_BadRequest() throws Exception { .andExpect(status().isBadRequest()); } @Test + @WithMockUser(username = "member") void testBorrowBook() throws Exception { UUID customerId = customer.getCustomerId(); UUID bookId = book.getBookId(); @@ -137,6 +138,7 @@ void testBorrowBook() throws Exception { assertEquals(1, transactionCount); } @Test + @WithMockUser(username = "member") void testBorrowBook_bookNotAvailable() throws Exception { UUID customerId = customer.getCustomerId(); UUID bookId = book.getBookId(); @@ -292,4 +294,16 @@ public void tearDown() { bookRepository.deleteAll(); authorRepository.deleteAll(); } + + /** A member borrowing against somebody else's membership would spend their loan limit. */ + @Test + @WithMockUser(username = "member", roles = "USER") + void borrowingAgainstAnotherMembershipIsRefused() throws Exception { + Customer otherMember = customerUseCase.createNewCustomer( + new CreateNewCustomer("Someone Else", "someone.else@example.com", true)); + + mockMvc.perform(post("/transactions/borrowBook/" + otherMember.getCustomerId() + "/" + + book.getBookId())) + .andExpect(status().isForbidden()); + } } \ No newline at end of file