diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index 34ea3cb..0a979ea 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -1 +1 @@ -coverage91.7% \ No newline at end of file +coverage91.1% \ No newline at end of file diff --git a/src/main/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpoint.java b/src/main/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpoint.java new file mode 100644 index 0000000..6fcfb53 --- /dev/null +++ b/src/main/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpoint.java @@ -0,0 +1,67 @@ +package ca.bc.gov.backendstartapi.endpoint; + +import ca.bc.gov.backendstartapi.entity.FundingSource; +import ca.bc.gov.backendstartapi.repository.FundingSourceRepository; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.LocalDate; +import java.time.temporal.TemporalUnit; +import java.util.List; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** This class exposes funding sources resources API. */ +@Setter +@RestController +@NoArgsConstructor +@RequestMapping("/api/funding-sources") +@Tag( + name = "FundingSourceEndpoint", + description = "Resource to retrieve Funding Source to Owners Agencies") +public class FundingSourceEndpoint { + + private FundingSourceRepository fundingSourceRepository; + + @Autowired + public FundingSourceEndpoint(FundingSourceRepository fundingSourceRepository) { + this.fundingSourceRepository = fundingSourceRepository; + } + + /** + * Retrieve all funding sources. + * + * @return A list of {@link FundingSource} with all found result. + */ + @GetMapping(produces = "application/json") + @PreAuthorize("hasRole('user_read')") + @Operation( + summary = "Retrieve non-expired funding sources", + description = "Retrieve all valid (non expired) funding source based on effectiveDate " + + "and expiryDate, where 'today >= effectiveDate' and 'today < expiryDate'.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Returns a list containing all valid (non expired) funding sources", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = FundingSource.class))), + @ApiResponse( + responseCode = "401", + description = "Access token is missing or invalid", + content = @Content(schema = @Schema(implementation = Void.class))) + }) + public List getAllValidFundingSources() { + return fundingSourceRepository.findAllValid(); + } +} diff --git a/src/main/java/ca/bc/gov/backendstartapi/entity/FundingSource.java b/src/main/java/ca/bc/gov/backendstartapi/entity/FundingSource.java new file mode 100644 index 0000000..3e2a600 --- /dev/null +++ b/src/main/java/ca/bc/gov/backendstartapi/entity/FundingSource.java @@ -0,0 +1,44 @@ +package ca.bc.gov.backendstartapi.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; + +/** This class presents a funding source to an agency seedlot owner. */ +@Getter +@Setter +@Entity +@Table(name = "SPAR_FUND_SRCE_CODE") +@Schema(description = "Represents a Funding Source object in the database") +public class FundingSource { + + @Id + @Column(name = "SPAR_FUND_SRCE_CODE") + @Schema(description = "Funding source's code, from SPAR_FUND_SRCE_CODE column", example = "BCT") + private String code; + + @Column(name = "DESCRIPTION") + @Schema( + description = "Funding source's description, from DESCRIPTION column", + example = "BC Timber Sales") + private String description; + + @Column(name = "EFFECTIVE_DATE") + @Schema( + description = "Funding source's effective date.", + type = "string", + format = "date") + private LocalDate effectiveDate; + + @Column(name = "EXPIRY_DATE") + @Schema( + description = "Funding source's expiry date.", + type = "string", + format = "date") + private LocalDate expiryDate; +} diff --git a/src/main/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepository.java b/src/main/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepository.java new file mode 100644 index 0000000..5cf6a2f --- /dev/null +++ b/src/main/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepository.java @@ -0,0 +1,16 @@ +package ca.bc.gov.backendstartapi.repository; + +import ca.bc.gov.backendstartapi.entity.FundingSource; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +/** This interface enables the funding source entity to be retrieved from the database. */ +public interface FundingSourceRepository extends JpaRepository { + + @Query( + value = + "select fs from FundingSource fs WHERE CURRENT_DATE >= fs.effectiveDate " + + "AND CURRENT_DATE < fs.expiryDate ORDER BY fs.code") + List findAllValid(); +} diff --git a/src/test/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpointTest.java b/src/test/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpointTest.java new file mode 100644 index 0000000..d374c82 --- /dev/null +++ b/src/test/java/ca/bc/gov/backendstartapi/endpoint/FundingSourceEndpointTest.java @@ -0,0 +1,97 @@ +package ca.bc.gov.backendstartapi.endpoint; + +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ca.bc.gov.backendstartapi.entity.FundingSource; +import ca.bc.gov.backendstartapi.repository.FundingSourceRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +@ExtendWith(SpringExtension.class) +@WebMvcTest(FundingSourceEndpoint.class) +class FundingSourceEndpointTest { + + @Autowired private MockMvc mockMvc; + + @MockBean private FundingSourceRepository fundingSourceRepository; + + @Test + @DisplayName("findAllSuccessTest") + @WithMockUser(roles = "user_read") + void findAllSuccessTest() throws Exception { + FundingSource fundingSourceBct = new FundingSource(); + fundingSourceBct.setCode("BCT"); + fundingSourceBct.setDescription("BC Timber Sales"); + fundingSourceBct.setEffectiveDate(LocalDate.parse("2003-04-01")); + fundingSourceBct.setExpiryDate(LocalDate.parse("9999-12-31")); + + FundingSource fundingSourceCbi = new FundingSource(); + fundingSourceCbi.setCode("CBI"); + fundingSourceCbi.setDescription("Carbon Offset Investment"); + fundingSourceCbi.setEffectiveDate(LocalDate.parse("2013-08-01")); + fundingSourceCbi.setExpiryDate(LocalDate.parse("9999-12-31")); + + FundingSource fundingSourceCl = new FundingSource(); + fundingSourceCl.setCode("CL"); + fundingSourceCl.setDescription("Catastrophic Losses"); + fundingSourceCl.setEffectiveDate(LocalDate.parse("1905-01-01")); + fundingSourceCl.setExpiryDate(LocalDate.parse("2099-09-30")); + + List sources = new ArrayList<>(); + sources.add(fundingSourceBct); + sources.add(fundingSourceCbi); + sources.add(fundingSourceCl); + + when(fundingSourceRepository.findAllValid()).thenReturn(sources); + + mockMvc + .perform( + get("/api/funding-sources") + .with(csrf().asHeader()) + .header("Content-Type", "application/json") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].code").value("BCT")) + .andExpect(jsonPath("$[0].description").value("BC Timber Sales")) + .andExpect(jsonPath("$[0].effectiveDate").value("2003-04-01")) + .andExpect(jsonPath("$[0].expiryDate").value("9999-12-31")) + .andExpect(jsonPath("$[1].code").value("CBI")) + .andExpect(jsonPath("$[1].description").value("Carbon Offset Investment")) + .andExpect(jsonPath("$[1].effectiveDate").value("2013-08-01")) + .andExpect(jsonPath("$[1].expiryDate").value("9999-12-31")) + .andExpect(jsonPath("$[2].code").value("CL")) + .andExpect(jsonPath("$[2].description").value("Catastrophic Losses")) + .andExpect(jsonPath("$[2].effectiveDate").value("1905-01-01")) + .andExpect(jsonPath("$[2].expiryDate").value("2099-09-30")) + .andReturn(); + } + + @Test + @DisplayName("findAllNoAuthorizedTest") + void findAllNoAuthorizedTest() throws Exception { + mockMvc + .perform( + get("/api/funding-sources") + .with(csrf().asHeader()) + .header("Content-Type", "application/json") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is(401)) + .andReturn(); + } + +} diff --git a/src/test/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepositoryTest.java b/src/test/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepositoryTest.java new file mode 100644 index 0000000..1841298 --- /dev/null +++ b/src/test/java/ca/bc/gov/backendstartapi/repository/FundingSourceRepositoryTest.java @@ -0,0 +1,60 @@ +package ca.bc.gov.backendstartapi.repository; + +import ca.bc.gov.backendstartapi.entity.FundingSource; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@DataJpaTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +class FundingSourceRepositoryTest { + + @Autowired private FundingSourceRepository fundingSourceRepository; + + private boolean isValid(FundingSource fundingSource) { + LocalDate today = LocalDate.now(); + + // Effective date - Should be before or same as today + if (fundingSource.getEffectiveDate().isAfter(today)) { + return false; + } + + // Expiry date - Should be after today + return fundingSource.getExpiryDate().isAfter(today); + } + + @Test + @DisplayName("findAllTest") + @Sql(scripts = {"classpath:scripts/FundingSourceRepositoryTest_findAllTest.sql"}) + void findAllTest() { + List sources = fundingSourceRepository.findAllValid(); + + Assertions.assertFalse(sources.isEmpty()); + Assertions.assertEquals(3, sources.size()); + + FundingSource fundingSourceBct = sources.get(0); + Assertions.assertEquals("BCT", fundingSourceBct.getCode()); + Assertions.assertEquals("BC Timber Sales", fundingSourceBct.getDescription()); + Assertions.assertTrue(isValid(fundingSourceBct)); + + FundingSource fundingSourceCbi = sources.get(1); + Assertions.assertEquals("CBI", fundingSourceCbi.getCode()); + Assertions.assertEquals("Carbon Offset Investment", fundingSourceCbi.getDescription()); + Assertions.assertTrue(isValid(fundingSourceCbi)); + + FundingSource fundingSourceCl = sources.get(2); + Assertions.assertEquals("CL", fundingSourceCl.getCode()); + Assertions.assertEquals("Catastrophic Losses", fundingSourceCl.getDescription()); + Assertions.assertTrue(isValid(fundingSourceCl)); + } +} diff --git a/src/test/resources/scripts/FundingSourceRepositoryTest_findAllTest.sql b/src/test/resources/scripts/FundingSourceRepositoryTest_findAllTest.sql new file mode 100644 index 0000000..035b9b9 --- /dev/null +++ b/src/test/resources/scripts/FundingSourceRepositoryTest_findAllTest.sql @@ -0,0 +1,13 @@ +INSERT INTO SPAR_FUND_SRCE_CODE (SPAR_FUND_SRCE_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE) + VALUES ('BCT', 'BC Timber Sales', '2003-04-01', '9999-12-31'); +INSERT INTO SPAR_FUND_SRCE_CODE (SPAR_FUND_SRCE_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE) + VALUES ('CBI', 'Carbon Offset Investment', '2013-08-01', '9999-12-31'); +INSERT INTO SPAR_FUND_SRCE_CODE (SPAR_FUND_SRCE_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE) + VALUES ('CL', 'Catastrophic Losses', '1905-01-01', '2099-09-30'); + +-- Not effective yet +INSERT INTO SPAR_FUND_SRCE_CODE (SPAR_FUND_SRCE_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE) + VALUES ('CI', 'Catastrophic Issues', '2033-01-01', '2099-09-30'); +-- Expired +INSERT INTO SPAR_FUND_SRCE_CODE (SPAR_FUND_SRCE_CODE, DESCRIPTION, EFFECTIVE_DATE, EXPIRY_DATE) + VALUES ('CO', 'Catastrophic Others', '2002-01-01', '2022-09-30'); \ No newline at end of file