Skip to content

Commit

Permalink
Showing 35 changed files with 1,527 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -6,10 +6,8 @@
import ca.bc.gov.app.service.client.ClientCodeService;
import ca.bc.gov.app.service.client.ClientCountryProvinceService;
import ca.bc.gov.app.service.client.ClientDistrictService;
import ca.bc.gov.app.service.client.ClientService;
import io.micrometer.observation.annotation.Observed;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -28,7 +26,6 @@
@Observed
public class ClientCodesController {

private final ClientService clientService;
private final ClientDistrictService clientDistrictService;
private final ClientCountryProvinceService clientCountryProvinceService;
private final ClientCodeService clientCodeService;
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package ca.bc.gov.app.controller.client;

import ca.bc.gov.app.ApplicationConstant;
import ca.bc.gov.app.dto.bcregistry.ClientDetailsDto;
import ca.bc.gov.app.dto.client.ClientListDto;
import ca.bc.gov.app.dto.client.ClientLookUpDto;
import ca.bc.gov.app.exception.NoClientDataFound;
import ca.bc.gov.app.service.client.ClientLegacyService;
import ca.bc.gov.app.service.client.ClientService;
import ca.bc.gov.app.util.JwtPrincipalUtil;
import io.micrometer.observation.annotation.Observed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import org.apache.commons.text.WordUtils;
import org.springframework.data.util.Pair;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -27,6 +33,7 @@
public class ClientController {

private final ClientService clientService;
private final ClientLegacyService clientLegacyService;

@GetMapping("/{clientNumber}")
public Mono<ClientDetailsDto> getClientDetails(
@@ -45,7 +52,44 @@ public Mono<ClientDetailsDto> getClientDetails(
JwtPrincipalUtil.getProvider(principal)
);
}

@GetMapping("/search")
public Flux<ClientListDto> fullSearch(
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "10") int size,
@RequestParam(required = false, defaultValue = "") String keyword,
ServerHttpResponse serverResponse) {

log.info("Listing clients: page={}, size={}, keyword={}", page, size, keyword);

return clientLegacyService
.search(
page,
size,
keyword
)
.doOnNext(pair -> {
Long count = pair.getSecond();

serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of(count.toString())
);
}
)
.map(Pair::getFirst)
.doFinally(signalType ->
serverResponse
.getHeaders()
.putIfAbsent(
ApplicationConstant.X_TOTAL_COUNT,
List.of("0")
)
);
}

/**
* Retrieve a Flux of ClientLookUpDto objects by searching for clients with a specific name.
*
10 changes: 10 additions & 0 deletions backend/src/main/java/ca/bc/gov/app/dto/client/ClientListDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ca.bc.gov.app.dto.client;

public record ClientListDto(
String clientNumber,
String clientAcronym,
String clientFullName,
String clientType,
String city,
String clientStatus) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ca.bc.gov.app.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.server.ResponseStatusException;

@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public class MissingRequiredParameterException extends ResponseStatusException {
public MissingRequiredParameterException(String parameterName) {
super(HttpStatus.EXPECTATION_FAILED,
String.format("Missing value for parameter %s", parameterName));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ca.bc.gov.app.service.client;

import ca.bc.gov.app.dto.client.ClientListDto;
import ca.bc.gov.app.dto.legacy.AddressSearchDto;
import ca.bc.gov.app.dto.legacy.ContactSearchDto;
import ca.bc.gov.app.dto.legacy.ForestClientDto;
@@ -11,6 +12,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
@@ -307,6 +309,38 @@ public Flux<ForestClientDto> searchContact(ContactSearchDto dto) {
client.clientNumber())
);
}

public Flux<Pair<ClientListDto, Long>> search(int page, int size, String keyword) {
log.info(
"Searching clients by keyword {} with page {} and size {}",
keyword,
page,
size
);

return legacyApi
.get()
.uri(builder ->
builder
.path("/api/search")
.queryParam("page", page)
.queryParam("size", size)
.queryParam("value", keyword)
.build(Map.of())
)
.exchangeToFlux(response -> {
List<String> totalCountHeader = response.headers().header("X-Total-Count");
Long count = totalCountHeader.isEmpty() ? 0L : Long.valueOf(totalCountHeader.get(0));

return response
.bodyToFlux(ClientListDto.class)
.map(dto -> Pair.of(dto, count));
})
.doOnNext(pair -> {
ClientListDto dto = pair.getFirst();
Long totalCount = pair.getSecond();
log.info("Found clients by keyword {}, total count: {}", dto.clientNumber(), totalCount);
});
}

}
Original file line number Diff line number Diff line change
@@ -11,13 +11,11 @@
import ca.bc.gov.app.dto.client.ClientContactDto;
import ca.bc.gov.app.dto.client.ClientListSubmissionDto;
import ca.bc.gov.app.dto.client.ClientSubmissionDto;
import ca.bc.gov.app.dto.client.DistrictDto;
import ca.bc.gov.app.dto.submissions.SubmissionAddressDto;
import ca.bc.gov.app.dto.submissions.SubmissionApproveRejectDto;
import ca.bc.gov.app.dto.submissions.SubmissionBusinessDto;
import ca.bc.gov.app.dto.submissions.SubmissionContactDto;
import ca.bc.gov.app.dto.submissions.SubmissionDetailsDto;
import ca.bc.gov.app.entity.client.ClientTypeCodeEntity;
import ca.bc.gov.app.entity.client.SubmissionDetailEntity;
import ca.bc.gov.app.entity.client.SubmissionEntity;
import ca.bc.gov.app.entity.client.SubmissionLocationContactEntity;
@@ -30,7 +28,6 @@
import ca.bc.gov.app.predicates.QueryPredicates;
import ca.bc.gov.app.predicates.SubmissionDetailPredicates;
import ca.bc.gov.app.predicates.SubmissionPredicates;
import ca.bc.gov.app.repository.client.DistrictCodeRepository;
import ca.bc.gov.app.repository.client.SubmissionContactRepository;
import ca.bc.gov.app.repository.client.SubmissionDetailRepository;
import ca.bc.gov.app.repository.client.SubmissionLocationContactRepository;
134 changes: 134 additions & 0 deletions frontend/cypress/e2e/pages/SearchPage.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { ClientSearchResult } from "@/dto/CommonTypesDto";

describe("Search Page", () => {
const predictiveSearchCounter = {
count: 0,
};

const checkDisplayedResults = (clientList: ClientSearchResult[]) => {
clientList.forEach((client) => {
cy.get("#search-box")
.find(`cds-combo-box-item[data-value^="${client.clientNumber}"]`)
.should("exist");
});
};
beforeEach(() => {
// reset counter
predictiveSearchCounter.count = 0;

cy.intercept("/api/clients/search?keyword=*", (req) => {
predictiveSearchCounter.count++;
req.continue();
}).as("predictiveSearch");

cy.viewport(1920, 1080);
cy.visit("/");

cy.login("uattest@gov.bc.ca", "Uat Test", "idir", {
given_name: "James",
family_name: "Baxter",
"cognito:groups": ["CLIENT_VIEWER"],
});

// Check if the Client search button is visible
cy.get("#menu-list-search").should("be.visible").click();

cy.get("h1").should("be.visible").should("contain", "Client search");

cy.window().then((win) => {
cy.stub(win, "open").as("windowOpen");
});
});

describe("when user fills in the search box with a valid value", () => {
beforeEach(() => {
cy.fillFormEntry("#search-box", "car", { skipBlur: true });
});

it("makes the API call with the entered keywords", () => {
cy.wait("@predictiveSearch").then((interception) => {
expect(interception.request.query.keyword).to.eq("car");
});
cy.wrap(predictiveSearchCounter).its("count").should("eq", 1);
});

it("displays autocomplete results", () => {
cy.wait("@predictiveSearch").then((interception) => {
const data = interception.response.body;

cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0);

cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length", data.length)
.should("be.visible");

checkDisplayedResults(data);
});
});

describe("and types more characters", () => {
beforeEach(() => {
cy.wait("@predictiveSearch");
cy.fillFormEntry("#search-box", "d", { skipBlur: true });
});

it("makes another the API call with the updated keywords", () => {
cy.wait("@predictiveSearch").then((interception) => {
expect(interception.request.query.keyword).to.eq("card");
});
cy.wrap(predictiveSearchCounter).its("count").should("eq", 2);
});

it("updates the autocomplete results", () => {
cy.wait("@predictiveSearch").then((interception) => {
const data = interception.response.body;

cy.wrap(data).should("be.an", "array").and("have.length.greaterThan", 0);

cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length", data.length)
.should("be.visible");

checkDisplayedResults(data);
});
});
});

describe("and user clicks a result", () => {
const clientNumber = "00054076";
beforeEach(() => {
cy.get("#search-box")
.find("cds-combo-box-item")
.should("have.length.greaterThan", 0)
.should("be.visible");

cy.get("#search-box").find(`cds-combo-box-item[data-value^="${clientNumber}"]`).click();
});
it("navigates to the client details", () => {
const greenDomain = "green-domain.com";
cy.get("@windowOpen").should(
"be.calledWith",
`https://${greenDomain}/int/client/client02MaintenanceAction.do?bean.clientNumber=${clientNumber}`,
"_blank",
"noopener",
);
});
});
});

describe("when user fills in the search box with an invalid value", () => {
beforeEach(() => {
cy.fillFormEntry("#search-box", "até", { skipBlur: true });
});

it("shows an error message", () => {
cy.contains("The search terms can only contain: A-Z, a-z, 0-9, space or common symbols");
});
it("makes no API call", () => {
cy.wait(500); // This time has to be greater than the debouncing time
cy.wrap(predictiveSearchCounter).its("count").should("eq", 0);
});
});
});
46 changes: 38 additions & 8 deletions frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -146,15 +146,45 @@ Cypress.Commands.add("getMany", (names: string[]): Cypress.Chainable<any[]> => {
return cy.wrap(values);
});

Cypress.Commands.add("fillFormEntry",(field: string, value: string, delayMS: number = 10, area: boolean = false) =>{
interface FillFormEntryOptions {
delayMS?: number;
area?: boolean;
skipBlur?: boolean;
}

interface FillFormEntry {
(field: string, value: string, delayMS?: number, area?: boolean): void;
(field: string, value: string, options?: FillFormEntryOptions): void;
}

const fillFormEntry: FillFormEntry = (
field: string,
value: string,
arg3: number | FillFormEntryOptions = 10,
arg4: boolean | never = false,
) => {
const options =
typeof arg3 === "object"
? arg3
: {
delayMS: arg3,
area: arg4,
};
const { delayMS, area, skipBlur } = options;
cy.get(field)
.should("exist")
.shadow()
.find(area ? "textarea" : "input")
.focus()
.type(value,{ delay: delayMS })
.blur();
});
.should("exist")
.shadow()
.find(area ? "textarea" : "input")
.focus()
.type(value, { delay: delayMS })
.then((subject) => {
if (!skipBlur) {
cy.wrap(subject).blur();
}
});
};

Cypress.Commands.add("fillFormEntry", fillFormEntry);

Cypress.Commands.add("clearFormEntry",(field: string, area: boolean = false) =>{
cy.get(field)
Loading

0 comments on commit abb5d03

Please sign in to comment.