Repository: RAF-SI-2024/Banka-4-Backend
Pull request #270: user-service-fix: card search account number filter
Author: login: VasilijeJukic01
Labels: name: approved
name: lgtm
name: size/L
Created: 2025-04-02T21:33:36Z | Merged: 2025-04-03T20:19:21Z



Diff:
diff --git a/user-service/src/main/java/rs/banka4/user_service/repositories/CardRepository.java b/user-service/src/main/java/rs/banka4/user_service/repositories/CardRepository.java
index 77bd0474d..22583d01a 100644
--- a/user-service/src/main/java/rs/banka4/user_service/repositories/CardRepository.java
+++ b/user-service/src/main/java/rs/banka4/user_service/repositories/CardRepository.java
@@ -8,6 +8,7 @@
 import org.springframework.stereotype.Repository;
 import rs.banka4.user_service.domain.account.db.Account;
 import rs.banka4.user_service.domain.card.db.Card;
+import rs.banka4.user_service.domain.user.client.db.Client;
 
 @Repository
 public interface CardRepository extends JpaRepository<Card, UUID>, JpaSpecificationExecutor<Card> {
@@ -23,4 +24,6 @@ public interface CardRepository extends JpaRepository<Card, UUID>, JpaSpecificat
 
     List<Card> findByAccountAccountNumber(String accountNumber);
 
+    List<Card> findByAccount_Client(Client client);
+
 }
diff --git a/user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java b/user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java
index f84b1a583..a86b2bd03 100644
--- a/user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java
+++ b/user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java
@@ -5,7 +5,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Primary;
@@ -201,15 +200,20 @@ public ResponseEntity<Page<CardDto>> clientSearchCards(
 
         Set<Account> accounts = accountRepository.findAllByClient(client.get());
 
-        boolean found =
-            accounts.stream()
-                .map(Account::getAccountNumber)
-                .collect(Collectors.toSet())
-                .contains(accountNumber);
+        List<Card> clientCards;
 
-        if (!found) throw new NotAccountOwner();
+        if (accountNumber != null) {
+            boolean found =
+                accounts.stream()
+                    .map(Account::getAccountNumber)
+                    .anyMatch(accountNumber::equals);
 
-        List<Card> clientCards = cardRepository.findByAccountAccountNumber(accountNumber);
+            if (!found) throw new NotAccountOwner();
+
+            clientCards = cardRepository.findByAccountAccountNumber(accountNumber);
+        } else {
+            clientCards = cardRepository.findByAccount_Client(client.get());
+        }
 
         List<CardDto> cardDtos =
             clientCards.stream()
diff --git a/user-service/src/test/java/rs/banka4/user_service/integration/SearchClientCardsTest.java b/user-service/src/test/java/rs/banka4/user_service/integration/SearchClientCardsTest.java
new file mode 100644
index 000000000..9cbb0308f
--- /dev/null
+++ b/user-service/src/test/java/rs/banka4/user_service/integration/SearchClientCardsTest.java
@@ -0,0 +1,105 @@
+package rs.banka4.user_service.integration;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+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.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.assertj.MockMvcTester;
+import rs.banka4.testlib.integration.DbEnabledTest;
+import rs.banka4.user_service.domain.account.db.Account;
+import rs.banka4.user_service.integration.generator.UserGenerator;
+import rs.banka4.user_service.integration.seeder.TestDataSeeder;
+import rs.banka4.user_service.repositories.AccountRepository;
+import rs.banka4.user_service.repositories.ClientRepository;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@DbEnabledTest
+public class SearchClientCardsTest {
+
+    @Autowired
+    private MockMvcTester m;
+
+    @Autowired
+    private UserGenerator userGen;
+
+    @Autowired
+    private TestDataSeeder testDataSeeder;
+
+    @Autowired
+    private ClientRepository clientRepo;
+
+    @Autowired
+    private AccountRepository accountRepo;
+
+    private String accessToken;
+
+    @BeforeEach
+    void setUp() {
+        Account account = testDataSeeder.seedAccount();
+        testDataSeeder.seedActiveCard(account);
+
+        userGen.createClient(x -> x);
+        account.setClient(
+            clientRepo.findByEmail("john.doe@example.com")
+                .orElseThrow()
+        );
+        accountRepo.save(account);
+        var toks = userGen.doClientLogin("john.doe@example.com", "test");
+        accessToken = toks.accessToken();
+    }
+
+    @Test
+    void clientSearchCardsSuccessfully() throws Exception {
+        m.get()
+            .uri("/cards/client/search")
+            .header("Authorization", "Bearer " + accessToken)
+            .accept(MediaType.APPLICATION_JSON)
+            .assertThat()
+            .hasStatusOk()
+            .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON)
+            .bodyJson()
+            /*
+             * Be lenient. We're testing that the array is size one, and that we're looking at page
+             * three.
+             */
+            .isLenientlyEqualTo(
+                "testdata/SearchClientCardsTest_clientSearchCardsSuccessfully.json"
+            );
+    }
+
+    @Test
+    void clientSearchCardsAccountNumberFilterSuccessfully() throws Exception {
+        m.get()
+            .uri("/cards/client/search?accountNumber=123456789")
+            .header("Authorization", "Bearer " + accessToken)
+            .accept(MediaType.APPLICATION_JSON)
+            .assertThat()
+            .hasStatusOk()
+            .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON)
+            .bodyJson()
+            /*
+             * Be lenient. We're testing that the array is size one, and that we're looking at page
+             * three.
+             */
+            .isLenientlyEqualTo(
+                "testdata/SearchClientCardsTest_clientSearchCardsSuccessfully.json"
+            );
+    }
+
+    @Test
+    void clientSearchCards_failsWhenNotOwnerOfAccount() {
+        String fakeAccountNumber = "9999999999";
+
+        m.get()
+            .uri("/cards/client/search?accountNumber=" + fakeAccountNumber)
+            .header("Authorization", "Bearer " + accessToken)
+            .accept(MediaType.APPLICATION_JSON)
+            .assertThat()
+            .hasStatus(HttpStatus.FORBIDDEN);
+    }
+
+}
diff --git a/user-service/src/test/java/rs/banka4/user_service/integration/generator/UserGenerator.java b/user-service/src/test/java/rs/banka4/user_service/integration/generator/UserGenerator.java
index 5df6e9b71..79a4bb07e 100644
--- a/user-service/src/test/java/rs/banka4/user_service/integration/generator/UserGenerator.java
+++ b/user-service/src/test/java/rs/banka4/user_service/integration/generator/UserGenerator.java
@@ -2,6 +2,7 @@
 
 import java.time.LocalDate;
 import java.util.HashSet;
+import java.util.UUID;
 import lombok.AllArgsConstructor;
 import org.springframework.stereotype.Service;
 import rs.banka4.user_service.domain.auth.dtos.LoginDto;
@@ -12,6 +13,7 @@
 import rs.banka4.user_service.domain.user.employee.db.Employee;
 import rs.banka4.user_service.repositories.ClientRepository;
 import rs.banka4.user_service.repositories.EmployeeRepository;
+import rs.banka4.user_service.service.abstraction.ClientService;
 import rs.banka4.user_service.service.abstraction.EmployeeService;
 
 /**
@@ -103,6 +105,21 @@ public LoginResponseDto doEmployeeLogin(String email, String password) {
         return employeeService.login(new LoginDto(email, password));
     }
 
+    private final ClientService clientService;
+
+    /**
+     * Create and return the response of a login request for a client with email {@code email} and
+     * password {@code password}.
+     *
+     * @param email Client email.
+     * @param password Client password.
+     * @throws Throwable If login fails.
+     * @returns Tokens produced by logging in.
+     */
+    public LoginResponseDto doClientLogin(String email, String password) {
+        return clientService.login(new LoginDto(email, password));
+    }
+
     private ClientRepository clientRepo;
 
     /**
@@ -113,6 +130,7 @@ public LoginResponseDto doEmployeeLogin(String email, String password) {
      *
      * {@snippet :
      * Client.builder()
+     *     .id(UUID.fromString("6f72db23-afc8-4d71-b392-eb9e626ed9af"))
      *     .firstName("John")
      *     .lastName("Doe")
      *     .dateOfBirth(LocalDate.of(1990, 1, 1))
@@ -135,6 +153,7 @@ public void createClient(UserCustomizer<Client.ClientBuilder<?, ?>> customizer)
             customizer.apply(
                 /* Keep in sync with the Javadoc above. */
                 Client.builder()
+                    .id(UUID.fromString("6f72db23-afc8-4d71-b392-eb9e626ed9af"))
                     .firstName("John")
                     .lastName("Doe")
                     .dateOfBirth(LocalDate.of(1990, 1, 1))
diff --git a/user-service/src/test/resources/testdata/SearchClientCardsTest_clientSearchCardsSuccessfully.json b/user-service/src/test/resources/testdata/SearchClientCardsTest_clientSearchCardsSuccessfully.json
new file mode 100644
index 000000000..d30c417b3
--- /dev/null
+++ b/user-service/src/test/resources/testdata/SearchClientCardsTest_clientSearchCardsSuccessfully.json
@@ -0,0 +1,34 @@
+{
+  "content": [
+    {
+      "cardNumber": "1234567810345678",
+      "cvv": "123",
+      "cardName": "VISA",
+      "creationDate": "2025-04-03",
+      "expirationDate": "2030-04-03",
+      "cardType": "DEBIT",
+      "limit": 10000,
+      "cardStatus": "ACTIVATED",
+      "accountNumber": "123456789",
+      "client": {
+        "id": "6f72db23-afc8-4d71-b392-eb9e626ed9af",
+        "firstName": "John",
+        "lastName": "Doe",
+        "dateOfBirth": "1990-01-01",
+        "gender": "MALE",
+        "email": "john.doe@example.com",
+        "phone": "+381664013258",
+        "address": "123 Main St",
+        "privileges": ["ADMIN"],
+        "has2FA": null
+      },
+      "authorizedUserDto": null
+    }
+  ],
+  "page": {
+    "size": 10,
+    "number": 0,
+    "totalElements": 1,
+    "totalPages": 1
+  }
+}


Reviews:
user: login: ArsenArsen
body: also there's no test - please add integration tests for the newfound dichotomy 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:46:28Z
user: login: ArsenArsen
body: /hold cancel
/lgtm

thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T20:11:02Z

Review comments:
diff_hunk: @@ -201,15 +201,21 @@ public ResponseEntity<Page<CardDto>> clientSearchCards(
 
         Set<Account> accounts = accountRepository.findAllByClient(client.get());
 
-        boolean found =
-            accounts.stream()
-                .map(Account::getAccountNumber)
-                .collect(Collectors.toSet())
-                .contains(accountNumber);
+        List<Card> clientCards;
 
-        if (!found) throw new NotAccountOwner();
+        if (accountNumber != null) {
+            boolean found =
+                accounts.stream()
+                    .map(Account::getAccountNumber)
+                    .collect(Collectors.toSet())
+                    .contains(accountNumber);
path: user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java
user: login: ArsenArsen
body: `.anyMatches(accountNumber::equals)`
created_at: 2025-04-02T21:43:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -201,15 +201,21 @@ public ResponseEntity<Page<CardDto>> clientSearchCards(
 
         Set<Account> accounts = accountRepository.findAllByClient(client.get());
 
-        boolean found =
-            accounts.stream()
-                .map(Account::getAccountNumber)
-                .collect(Collectors.toSet())
-                .contains(accountNumber);
+        List<Card> clientCards;
 
-        if (!found) throw new NotAccountOwner();
+        if (accountNumber != null) {
+            boolean found =
+                accounts.stream()
+                    .map(Account::getAccountNumber)
+                    .collect(Collectors.toSet())
+                    .contains(accountNumber);
 
-        List<Card> clientCards = cardRepository.findByAccountAccountNumber(accountNumber);
+            if (!found) throw new NotAccountOwner();
+
+            clientCards = cardRepository.findByAccountAccountNumber(accountNumber);
+        } else {
+            clientCards = cardRepository.findByAccount_Client(client.get());
path: user-service/src/main/java/rs/banka4/user_service/service/impl/CardServiceImpl.java
user: login: ArsenArsen
body: do accounts always have a client?
created_at: 2025-04-02T21:45:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: raf-si-2024-prow[bot]
created_at: 2025-04-02T21:33:41Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/RAF-SI-2024/Banka-4-Backend/pull/270#" title="Author self-approved">VasilijeJukic01</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=RAF-SI-2024%2FBanka-4-Backend).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/RAF-SI-2024/Banka-4-Backend/blob/main/OWNERS)~~ [VasilijeJukic01]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: ognjend05
created_at: 2025-04-02T22:05:49Z
body: > also there's no test - please add integration tests for the newfound dichotomy

It's not my feature, the person who worked need write a integration test
author_association: CONTRIBUTOR
user: login: ArsenArsen
created_at: 2025-04-02T22:24:48Z
body: > > also there's no test - please add integration tests for the newfound dichotomy
> 
> It's not my feature, the person who worked need write a integration test

your patch, your test
author_association: CONTRIBUTOR
user: login: ognjend05
created_at: 2025-04-02T22:59:25Z
body: > > > also there's no test - please add integration tests for the newfound dichotomy
> > 
> > 
> > It's not my feature, the person who worked need write a integration test
> 
> your patch, your test

We agree to disagree.
author_association: CONTRIBUTOR
user: login: ArsenArsen
created_at: 2025-04-02T23:06:21Z
body: /hold
author_association: CONTRIBUTOR
user: login: mehmedalijaK
created_at: 2025-04-03T16:04:48Z
body: @ognjend05 If I remember correctly, @danilojoncic and @iatanasijevic621rn were initially tasked with setting up the integration tests. Naturally, they couldn't cover the entire project from the start.

We agreed to gradually add integration tests as new features are introduced or bugs are fixed.

We understand this might not be your functionality originally, but we aim to cover everything we modify—whether it's our own code or someone else's—with proper tests. This serves as a kind of guarantee that the changes work as expected and don't break existing behavior. Treat it as a kind of safety net, not ownership burden.

Please make sure to include the relevant tests for this feature.
author_association: CONTRIBUTOR
user: login: raf-si-2024-prow[bot]
created_at: 2025-04-03T20:11:07Z
body: LGTM label has been added.  <details>Git tree hash: 0d9310e56af9157b0a51da69448edcd3f40f9350</details>
author_association: CONTRIBUTOR