Skip to content

Commit

Permalink
Expose Income Sources query API
Browse files Browse the repository at this point in the history
  • Loading branch information
semotpan committed Aug 8, 2024
1 parent c99aa03 commit 56084b3
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.myfinbox.income.adapter.web;

import io.myfinbox.income.application.IncomeSourceQuery;
import io.myfinbox.income.application.IncomeSourceService;
import io.myfinbox.income.domain.IncomeSource;
import io.myfinbox.rest.IncomeSourceResource;
Expand All @@ -21,6 +22,7 @@
final class IncomeSourceController implements IncomesSourceApi {

private final IncomeSourceService incomeSourceService;
private final IncomeSourceQuery incomeSourceQuery;
private final ApiFailureHandler apiFailureHandler;

@PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
Expand All @@ -43,6 +45,13 @@ public ResponseEntity<?> delete(@PathVariable UUID incomeSourceId) {
.fold(apiFailureHandler::handle, ok -> noContent().build());
}

@GetMapping
public ResponseEntity<?> list(@RequestParam(value = "accountId") UUID accountId) {
return ok(incomeSourceQuery.search(accountId).stream()
.map(this::toResource)
.toList());
}

private IncomeSourceResource toResource(IncomeSource incomeSource) {
return new IncomeSourceResource()
.incomeSourceId(incomeSource.getId().id())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
Expand All @@ -17,6 +18,7 @@
import java.util.UUID;

import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.QUERY;
import static org.springframework.http.HttpHeaders.LOCATION;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

Expand Down Expand Up @@ -81,4 +83,18 @@ ResponseEntity<?> update(@Parameter(in = PATH, description = "Income Source ID t
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> delete(@Parameter(in = PATH, description = "IncomeSourceID to be deleted", required = true) UUID incomeSourceId);

@Operation(summary = "Query a list of income sources for a specified account in the MyFinBox",
description = "Query a list of income sources for a specified account in the MyFinBox",
security = {@SecurityRequirement(name = "openId")},
tags = {TAG})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful Operation",
content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = IncomeSourceResource.class)))),
@ApiResponse(responseCode = "400", description = "Malformed JSON or Type Mismatch Failure",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "Internal Server Error",
content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
})
ResponseEntity<?> list(@Parameter(in = QUERY, description = "Account to be used for selecting income sources", required = true) UUID accountId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.myfinbox.income.application;

import io.myfinbox.income.domain.IncomeSource;

import java.util.List;
import java.util.UUID;

/**
* Interface for querying income source information.
*/
public interface IncomeSourceQuery {

/**
* Searches for income sources associated with a specific account.
*
* @param accountId the unique identifier of the account to search income sources for
* @return a list of {@link IncomeSource} objects associated with the specified account
*/
List<IncomeSource> search(UUID accountId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.myfinbox.income.application;

import io.myfinbox.income.domain.AccountIdentifier;
import io.myfinbox.income.domain.IncomeSource;
import io.myfinbox.income.domain.IncomeSources;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

import static java.util.Collections.emptyList;
import static java.util.Objects.isNull;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
class IncomeSourceQueryService implements IncomeSourceQuery {

private final IncomeSources incomeSources;

@Override
public List<IncomeSource> search(UUID accountId) {
if (isNull(accountId)) {
return emptyList();
}

return incomeSources.findByAccount(new AccountIdentifier(accountId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ class ExpenseCategoryControllerSpec extends Specification {

@Sql('/expense/web/expensecategory-create.sql')
def "should get a list with two expense category"() {
when: 'list expense category'
def response = getExpenses(UUID.fromString(DataSamples.accountId))
when: 'list expense categories'
def response = listExpenseCategories(UUID.fromString(DataSamples.accountId))

then: 'response status is OK'
assert response.getStatusCode() == OK
Expand Down Expand Up @@ -160,7 +160,7 @@ class ExpenseCategoryControllerSpec extends Specification {
)
}

def getExpenses(UUID accountId) {
def listExpenseCategories(UUID accountId) {
def uri = UriComponentsBuilder.fromUriString("${restTemplate.getRootUri()}/v1/expenses/category")
.queryParam("accountId", accountId)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class CategoryQueryServiceSpec extends Specification {
setup: 'mock the repository to return a list with a sample category for any account identifier'
1 * categories.findByAccount(_ as AccountIdentifier) >> [newSampleCategory()]

when: 'Searching for categories with a random account ID'
when: 'searching for categories with a random account ID'
def categoryList = service.search(UUID.randomUUID())

then: 'the result should be a list with one category'
assert categoryList.size() == 1
assert categoryList.containsAll(newSampleCategory())
}
}
4 changes: 4 additions & 0 deletions server/src/test/groovy/io/myfinbox/income/DataSamples.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ class DataSamples {
JsonOutput.toJson(INCOME_RESOURCE + map) as String
}

static newValidIncomeSourceResourceList() {
JsonOutput.toJson([INCOME_SOURCE_RESOURCE, INCOME_SOURCE_RESOURCE + [incomeSourceId: incomeSourceId2, name: 'Other']]) as String
}

static newValidIncomeCommand(map = [:]) {
MAPPER.readValue(JsonOutput.toJson(INCOME_COMMAND + map) as String, IncomeCommand.class)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ package io.myfinbox.income.adapter.web
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.myfinbox.TestServerApplication
import io.myfinbox.income.DataSamples
import org.skyscreamer.jsonassert.JSONAssert
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.jdbc.JdbcTestUtils
import org.springframework.web.util.UriComponentsBuilder
import spock.lang.Specification
import spock.lang.Tag

import static io.myfinbox.income.DataSamples.newValidIncomeSourceResource
import static io.myfinbox.income.DataSamples.*
import static org.skyscreamer.jsonassert.JSONCompareMode.LENIENT
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import static org.springframework.http.HttpMethod.DELETE
Expand Down Expand Up @@ -112,7 +113,7 @@ class IncomeSourceControllerSpec extends Specification {
}

@Sql(['/income/web/incomesource-create.sql', '/income/web/income-create.sql'])
def "Should fail delete when income source is in use"() {
def "should fail delete when income source is in use"() {
when: 'income source fails to delete'
def response = deleteIncomeSource()

Expand All @@ -123,13 +124,25 @@ class IncomeSourceControllerSpec extends Specification {
JSONAssert.assertEquals(expectDeleteConflictFailure(), response.getBody(), LENIENT)
}

@Sql('/income/web/incomesource-create.sql')
def "should get a list with two income source"() {
when: 'list income sources'
def response = listIncomeSources(UUID.fromString(accountId))

then: 'response status is OK'
assert response.getStatusCode() == OK

and: 'a list of two income sources is present'
JSONAssert.assertEquals(newValidIncomeSourceResourceList(), response.getBody(), LENIENT)
}

private postIncomeSource(String request) {
restTemplate.postForEntity('/v1/incomes/income-source', entityRequest(request), String.class)
}

private putIncomeSource(String request) {
restTemplate.exchange(
"/v1/incomes/income-source/${DataSamples.incomeSourceId}",
"/v1/incomes/income-source/${incomeSourceId}",
PUT,
entityRequest(request),
String.class
Expand All @@ -138,13 +151,27 @@ class IncomeSourceControllerSpec extends Specification {

private deleteIncomeSource() {
restTemplate.exchange(
"/v1/incomes/income-source/${DataSamples.incomeSourceId}",
"/v1/incomes/income-source/${incomeSourceId}",
DELETE,
entityRequest(null),
String.class
)
}

def listIncomeSources(UUID accountId) {
def uri = UriComponentsBuilder.fromUriString("${restTemplate.getRootUri()}/v1/incomes/income-source")
.queryParam("accountId", accountId)
.build()
.toUri()

restTemplate.exchange(
uri,
HttpMethod.GET,
null,
String.class
)
}

def expectedCreatedResource(ResponseEntity response) {
newValidIncomeSourceResource(
incomeSourceId: idFromLocation(response.getHeaders().getLocation())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.myfinbox.income.application

import io.myfinbox.income.domain.AccountIdentifier
import io.myfinbox.income.domain.IncomeSources
import spock.lang.Specification
import spock.lang.Tag

import static io.myfinbox.income.DataSamples.*

@Tag("unit")
class IncomeSourceQueryServiceSpec extends Specification {

IncomeSources incomeSources
IncomeSourceQueryService service

def setup() {
incomeSources = Mock()
service = new IncomeSourceQueryService(incomeSources)
}

def "should get empty list when income sources for provided account id not found"() {
setup: 'mock the repository to return an empty list for any account identifier'
1 * incomeSources.findByAccount(_ as AccountIdentifier) >> []

when: 'searching for income sources with a non-exiting account ID'
def incomeSourcesList = service.search(UUID.randomUUID())

then: 'the result should be an empty list'
assert incomeSourcesList.isEmpty()
}

def "should get empty list when account id is null"() {
when: 'searching for income sources with a null account ID'
def incomeSourcesList = service.search(null)

then: 'the result should be an empty list'
assert incomeSourcesList.isEmpty()

and: 'the repository should not be queried'
0 * incomeSources.findByAccount(_ as AccountIdentifier)
}

def "should get a list of income sources"() {
setup: 'mock the repository to return an empty list for any account identifier'
1 * incomeSources.findByAccount(_ as AccountIdentifier) >> [newSampleIncomeSource()]

when: 'searching for income sources with a random account ID'
def incomeSourcesList = service.search(UUID.randomUUID())

then: 'the result should be a list with one income source'
assert incomeSourcesList.containsAll(newSampleIncomeSource())
}
}
5 changes: 4 additions & 1 deletion server/src/test/resources/income/web/incomesource-create.sql
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
INSERT INTO server.income_source (id,
account_id,
creation_timestamp,
name)
VALUES ('3b257779-a5db-4e87-9365-72c6f8d4977d',
'e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'Bills'),
'2024-03-23T10:00:04.224870Z',
'Business'),
('e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'e2709aa2-7907-4f78-98b6-0f36a0c1b5ca',
'2024-03-23T10:00:04.224870Z',
'Other');

0 comments on commit 56084b3

Please sign in to comment.