First a save() method is executed which passes the test until it reaches a condition, where if it is true it calls the saveBankAccountAndRole() method and if it is false it sends a Mono.error(new Exception("...").
The sizeAccounts(String customerId) method does pass the test.
In the saveBankAccountAndRole(BankAccountDto bnkDto) method, after executing the sizeAccounts() method, the test does not pass, what am I missing?
public Flux<BankAccountDto> findAccountsByCustomerId(String customerId) {
return mongoRepository
.findAll()
.filter(ba ->
ba.getCustomerId().equals(customerId))
.map(AppUtils::entityToDto);
}
private Mono<Long> sizeAccounts(String customerId){
return findAccountsByCustomerId(customerId)
.count();
}
private Mono<BankAccountDto> saveBankAccountAndRole(BankAccountDto bnkDto) {
return sizeAccounts(bnkDto.getCustomerId())
.flatMap(number -> {
bnkDto.setOpeningOrder(number + 1);
return mongoRepository.save(AppUtils.dtoToEntity(bnkDto))
.switchIfEmpty(Mono.error(new Exception("Problem saving the bank account")))
.zipWhen(bnk -> {
var customerRoleDto = new CustomerRoleDto();
customerRoleDto.setBankAccountId(bnk.getBankAccountId());
customerRoleDto.setCustomerId(bnkDto.getCustomerId());
customerRoleDto.setRoleType(bnkDto.getRoleType());
return webClientRoleHelper.saveCustomerRole(customerRoleDto);
})
.switchIfEmpty(Mono.error(new Exception("Problem saving roles")))
.map(tuple -> AppUtils.entityToDto(tuple.getT1()));
});
}
test:
#Mock
private IMongoBankAccountRepository mongoRepository;
#InjectMocks
private BankAccountServiceImpl bankAccountServiceImpl;
#Test
void saveBankAccountAndRoleTest() throws Exception {
when(mongoRepository.findAll()).thenReturn(Flux.just(bnkDto)
.map(AppUtils::dtoToEntity));
when(mongoRepository.findAll().filter(ba ->
ba.getCustomerId().equals(customerId)))
.thenReturn(Flux.just(bnkDto).map(AppUtils::dtoToEntity));
StepVerifier.create(bankAccountServiceImpl.findAccountsByCustomerId(customerId))
.expectSubscription()
.expectNext(bnkDto)
.verifyComplete();
var spy = PowerMockito.spy(bankAccountServiceImpl);
PowerMockito.when(spy, "sizeAccounts", customerId)
.thenReturn(Mono.just(2L));
PowerMockito.when(spy, "saveBankAccountAndRole",bnkDto)
.thenReturn(Mono.just(bnkDto));
}
exception:
java.lang.AssertionError: expectation "expectNext(com.nttdata.bootcamp.model.dto.BankAccountDto#147c4523)" failed (expected value: com.nttdata.bootcamp.model.dto.BankAccountDto#147c4523; actual value: com.nttdata.bootcamp.model.dto.BankAccountDto#551725e4) at com.nttdata.bootcamp.business.impl.BankAccountServiceImplTest.saveBankAccountAndRoleTest(BankAccountServiceImplTest.java:267)
Which sends me when verifyComplete()
By looking at the code in your test, you shouldn't expect an specific object to be returned.
when(mongoRepository.findAll()).thenReturn(Flux.just(bnkDto)
.map(AppUtils::dtoToEntity));
when(mongoRepository.findAll().filter(ba ->
ba.getCustomerId().equals(customerId)))
.thenReturn(Flux.just(bnkDto).map(AppUtils::dtoToEntity));
The code above is mapping that DTO object to an Entity, which makes sense for a repository. However, that means that the following piece of code will "remap" it to a newly created object:
.zipWhen(bnk -> {
var customerRoleDto = new CustomerRoleDto();
customerRoleDto.setBankAccountId(bnk.getBankAccountId());
customerRoleDto.setCustomerId(bnkDto.getCustomerId());
customerRoleDto.setRoleType(bnkDto.getRoleType());
return webClientRoleHelper.saveCustomerRole(customerRoleDto);
})
Thus, you should be expecting an object with that same class, containing the same instance's variables values. But you can't expect it to be the exact same object.
You might want to try this (untested code):
StepVerifier.create(bankAccountServiceImpl.findAccountsByCustomerId(customerId))
.expectSubscription()
.expectMatches(dto -> dto.getBankAccountId().equals(bankDto.getBankAccountId) && dto.getCustomerId.equals(bnkDto.getCustomerId))
.verifyComplete();
I hope that works out for you.
Related
I'm evaluating Elsa for a new project at work, but have run into trouble when creating a service that DIs ISignaler, only when I also have a WorkflowContextProvider as indicated below.
My WorkflowProvider:
public class ContractorWorkflowContextProvider : WorkflowContextRefresher<ContractorRecruitVM>
{
private IContractorApi _contractorApi;
public ContractorWorkflowContextProvider(IContractorApi api)
{
_contractorApi = api;
}
public override async ValueTask<ContractorRecruitVM?> LoadAsync(LoadWorkflowContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(context.ContextId))
{
return null;
}
var ctrId = int.Parse(context.ContextId);
var vm = await _contractorApi.GetById(ctrId);
return vm;
}
...
}
ContractorApi:
public class ContractorApi : IContractorApi
{
IPersonRepo _personRepo;
IElsaDemoUOW _uow;
IElsaSignalService _elsaSignalService;
public ContractorApi(IPersonRepo personRepo, IElsaDemoUOW uow, IElsaSignalService elsaSignalService)
{
_personRepo = personRepo;
_uow = uow;
_elsaSignalService = elsaSignalService;
}
}
and my ElsaSignalService:
public class ElsaSignalService : IElsaSignalService
{
ISignaler _signaler;
public ElsaSignalService(ISignaler signaler)
{
_signaler = signaler;
}
public async Task<bool> CtrInitDocsUploaded(string workflowInstanceId)
{
var res = await _signaler.TriggerSignalAsync(signal: "ctr-init-docs-uploaded", workflowInstanceId: workflowInstanceId);
return true;
}
}
DI is setup like:
...
services
.AddElsa(elsa => elsa
.UseEntityFrameworkPersistence(ef => ef.UseSqlServer(elsaConnString, b => b.MigrationsAssembly("Elsa.Persistence.EntityFramework.SqlServer")))
.AddConsoleActivities()
.AddHttpActivities(elsaSection.GetSection("Server").Bind)
.AddQuartzTemporalActivities()
.AddJavaScriptActivities()
.AddWorkflowsFrom<Startup>()
.AddEmailActivities(elsaSection.GetSection("Smtp").Bind)
)
.AddWorkflowContextProvider<ContractorWorkflowContextProvider>();
...
services.AddTransient<IElsaDemoUOW, ElsaDemoUOW>();
services.AddTransient<IPersonRepo, PersonRepo>();
services.AddTransient<IContractorApi, ContractorApi>();
services.AddTransient<IElsaSignalService, ElsaSignalService>();
...
I'm not sure if I'm going about this correctly at all, but my goal is to have my "ElsaSignalService" that I'll be calling from the API, and that ElsaSignalService will raise whatever signal is appropriate.
Elsa will receive the signals, and in some/many cases save the workflowcontext back (using the custom provider I specified). If this is a valid setup, I'm not sure where the circular reference error is coming from, as I don't see one. If I'm misunderstanding how to structure Elsa to do what I'm after, would greatly appreciate input.
When I hit the app with the setup described above, I get the following circular dependency error but nothing I see looks like it's creating one.
A circular dependency was detected for the service of type 'Elsa.Services.IWorkflowLaunchpad'.
Elsa.Services.IWorkflowLaunchpad(Elsa.Services.Workflows.WorkflowLaunchpad) ->
Elsa.Services.IWorkflowRunner(Elsa.Services.Workflows.WorkflowRunner) ->
Elsa.Services.IWorkflowContextManager(Elsa.Services.WorkflowContexts.WorkflowContextManager) ->
System.Collections.Generic.IEnumerable<Elsa.Providers.WorkflowContexts.IWorkflowContextProvider> ->
Elsa.Providers.WorkflowContexts.IWorkflowContextProvider(ElsaDemo.DemoApp.WorkflowContexts.ContractorWorkflowContextProvider) ->
ElsaDemo.DemoApp.API.IContractorApi(ElsaDemo.DemoApp.API.ContractorApi) ->
ElsaDemo.DemoApp.Services.IElsaSignalService(ElsaDemo.DemoApp.Services.ElsaSignalService) ->
Elsa.Activities.Signaling.Services.ISignaler(Elsa.Activities.Signaling.Services.Signaler) ->
Elsa.Services.IWorkflowLaunchpad
Have below Method
private Mono<EventSlotBook> getTestEventSlotBook(EventUserAppt eventUserAppt){
Query query = new Query();
query.addCriteria(
new Criteria().andOperator(
Criteria.where("eventId").is(eventUserAppt.getEventId()),
Criteria.where("eventConfigId").is(eventUserAppt.getEventConfigId()),
Criteria.where("eventSlotId").is(eventUserAppt.getEventSlotId()),
Criteria.where("appointmentDate").in(eventUserAppt.getAppointmentDate()
)));
return this.reactiveMongoTemplate.findOne(query, EventSlotBook.class)
.flatMap(eventSlotExistingEntity -> {
if(eventSlotExistingEntity.getEventUsers() != null) {
eventSlotExistingEntity.getEventUsers().add(eventUserAppt.getEventUser());
}
return Mono.just(eventSlotExistingEntity);
})
.switchIfEmpty(getInitialTestEventSlotBook(eventUserAppt));
}
And above method called by
public Mono<EventSlotBookRequestDto> saveEventSlotBookFinal(Mono<EventSlotBookRequestDto> eventSlotBookRequestDtoMono){
log.info("Start::SaveEventSlotBook#######Final");
Mono<EventSlotBookRequestDto> eventDtoSaved =
eventSlotBookRequestDtoMono.map(AppUtils::dtoToEventUserApptEntity)
.flatMap(eventUserApptEntity -> getEventUserAppt(eventUserApptEntity))
.doOnNext(eventUserApptBeforeSave -> {
log.info("####BeforeSave::{}",eventUserApptBeforeSave);
})
.flatMap(eventUserApptRepository::save)
.doOnNext( eventUserApptAftereSave -> {
log.info("####AfterSave::{}",eventUserApptAftereSave);
})
.map(eventUserApptAfterSave -> getTestEventSlotBook(eventUserApptAfterSave)) -> IDE shows it returns Mono<Mono<EventSlotBoo>>
.flatMap(eventSlotBookrepository::save) --> Compile time error: o instance(s) of type variable(s) exist so that Mono<EventSlotBook> conforms to EventSlotBook
.map(eventSlotBooEntity -> AppUtils.entityToDto((EventSlotBook)eventSlotBooEntity));
log.info("End::SaveEventSlotBook#####Final");
return eventDtoSaved;
}
#Repository
public interface EventSlotBookRepository extends ReactiveMongoRepository<EventSlotBook,String> {
}
Not sure why .flatMap(eventSlotBookrepository::save) --> Compile time error: o instance(s) of type variable(s) exist so that Mono conforms to EventSlotBook it throws this error. flatMap expected flattened Mono<Mono> to EventSlotBook and save this data
ReactiveMongoRepository does not have a save method which would accept a Mono. It can only accept an instance of the entity type, so the following would work:
.flatMap(eventUserApptAfterSave -> getTestEventSlotBook(eventUserApptAfterSave)) // map changed to flatMap
.flatMap(eventSlotBookrepository::save)
I'm trying to do something again here in project reactor that I'm sure is reeeeeal simple for any of you project reactor gurus out there!
I've been searching and scratching around with this one for a while now, and feel I'm once again hitting a wall with this stuff.
All I'm trying to do is determine if a List of objects contained within a Mono is empty or not.
This is what I have so far:
private Mono<Boolean> isLastCardForAccount(String accountId) {
return cardService.getAccountCards(accountId)
.hasElement();
}
I'm thinking the above might work, but I'm having difficulty figuring out how to extract/access the 'Boolean' contained within the returned Mono. I think I have to use 'subscribe' somehow right?
I've mucked around with this stuff for a while now, but still no luck.
Here is how 'getAccountCards' is defined:
public Mono<List<Card>> getAccountCards(final String accountId) {
return cardCrudRepository.getCardsByAccountId(accountId)
.collectList();
}
From CardCrudRepository:
// #Query("SELECT * FROM card WHERE account_id = :accountId") <-Not sure if I need this
Flux<Card> getCardsByAccountId(String accountId);
And lastly, how I'm using 'isLastCardForAccount':
public Mono<Void> updateCardStatus(String accountId, String cardId, String cardStatus) {
return accountService.getAccount(accountId)
.map(Account::getClientId)
.map(clientId -> createUpdateCardStatusServiceRequestData(clientId, cardId, cardStatus))
.flatMap(requestData -> cartaClient.updateCardStatus(requestData)
.then(Mono.defer(() -> isCardBeingCancelled(cardStatus) ? allCardsCancelledForAccount(accountId) ? removeAccount(accountId) :
(isLostOrStolen(cardStatus) ? replaceCard(cardId, cardStatus).flatMap(this::updateCardNumber) : Mono.empty()) : Mono.empty())));
}
As always, any and all help and insight is tremendously appreciated!
I am not sure if this would resolve the issue but this way you can try to write your logic
return accountService.getAccount(accountId)
.map(Account::getClientId)
.map(clientId -> createUpdateCardStatusServiceRequestData(clientId, cardId, cardStatus))
.flatMap(requestData -> cartaClient.updateCardStatus(requestData)
.then(Mono.defer(() ->
Mono.zip(
Mono.just(isCardBeingCancelled(cardStatus)),
isLastCardForAccount(accountId),
Mono.just( isLostOrStolen(cardStatus) )
)
.map(tuple -> {
WRITE YOUR IF ELSE LOGIC
})
The idea is to use zip and then use the tuple for writing logic. The Tuple would be of type Tuple3 of <Boolean, Boolean ,Boolean>. I made the assumption that isLostOrStolen(cardStatus) returns Boolean.
One way of doing that is by using filterWhen operator like this:
.then(Mono.defer(() -> {
if (isCardBeingCancelled(cardStatus)) {
return Mono.just(accountId)
.filterWhen(this::allCardsCancelledForAccount)
.flatMap(this::removeAccount);
} else if (isLostOrStolen(cardStatus)) {
return replaceCard(cardId, cardStatus).flatMap(this::updateCardNumber);
}
return Mono.empty();
}))
You can use filterWhen in the case of asynchronous filtering. Check this section of Which operator do I need? reference and this How to filter Flux asynchronously.
As a side note, this is not going to work as you expect:
private Mono<Boolean> isLastCardForAccount(String accountId) {
return cardService.getAccountCards(accountId)
.hasElement();
}
public Mono<List<Card>> getAccountCards(final String accountId) {
return cardCrudRepository.getCardsByAccountId(accountId)
.collectList();
}
The collectList() will emit an empty List if there is no card. I'd use exists query instead:
public Mono<Boolean> isLastCardForAccount(final String accountId) {
return cardCrudRepository.existsByAccountId(accountId);
}
How can I test an extension function with Mockito? It doesn't seem to work nicely.
This is my extension function
fun <T> CrudRepository<T, String>.findOneById(id: String): T? {
val o = findById(id)
return if (o.isPresent) o.get() else null
}
And this is what I'm trying to test
#Test
fun getIslandById() {
//given
BDDMockito.given(islandRepository.findOneById("islandId1"))
.willReturn(IslandEntity(tileList, "1", "islandId1")) //findOneById is my extension function
//when
val island = islandService.getIslandById("islandId1")
//then
Assertions.assertThat(island?.id).isEqualTo("islandId1")
}
But the preceeding test throws the following error
org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
IslandEntity cannot be returned by findById()
findById() should return Optional
Any ideas?
Instance extension functions can be mocked like this with a little help of mockito-kotlin:
data class Bar(thing: Int)
class Foo {
fun Bar.bla(anotherThing: Int): Int { ... }
}
val bar = Bar(thing = 1)
val foo = mock<Foo>()
with(foo) {
whenever(any<Bar>().bla(any()).doReturn(3)
}
verify(foo).apply {
bar.bla(anotherThing = 2)
}
As I said in a comment above, in the bytecode extension functions are nothing more than static functions which accept receiver as a first argument. Therefore you can't mock an extension function with a Mockito since it is not able to mock static functions.
What you can do, in case that findById(id) is implemented by Repository and not another extension function, is next:
Mock return value of findById(id) instead.
Take a look at the sample code below:
#Test
fun getIslandById() {
//given
BDDMockito.given(islandRepository.findById("islandId1"))
.willReturn(Optional.of(IslandEntity(tileList, "1", "islandId1"))) //mock findById function
//when
val island = islandService.getIslandById("islandId1")
//then
Assertions.assertThat(island?.id).isEqualTo("islandId1")
}
This way you are indirectly mocking your extension by providing it a mocked value that you want it to operate on.
Note: Error that you posted above says that your findById(id) should return an optional. So, wrap your return value of findById(id) function with an optional by calling Optional.of(result).
I'm developing a app with Spring Boot 2.0 and Kotlin using the WebFlux framework.
I want to check if a user id exits before save a transaction. I'm stucked in a simple thing like validate if a Mono is empty.
fun createTransaction(serverRequest: ServerRequest) : Mono<ServerResponse> {
val transaction = serverRequest.body(BodyExtractors.toMono(Transaction::class.java))
transaction.flatMap {
val user = userRepository.findById(it.userId)
// If it's empty, return badRequest()
}
return transaction.flatMap { transactionRepository.save(it).then(created(URI.create("/transaction/" + it.id)).build()) }
}
It is possible to do what I want?
The techniques that allow checking whether Flux/Mono is empty
Using operators .switchIfEmpty/.defaultIfEmpty/Mono.repeatWhenEmpty
Using mentioned operators you will be able to react to the case when Stream has been completed without emitting any elements.
First of all, remember that operators such .map, .flatMap, .filter and many others will not be invoked at all if there no onNext has been invoked.
That means that in your case next code
transaction.flatMap {
val user = userRepository.findById(it.userId)
// If it's empty, return badRequest()
}
return transaction.flatMap { transactionRepository.save(it).then(created(URI.create("/transaction/" + it.id)).build()) }
will not be invoked at all, if transaction will be empty.
In case if there is a requirement for handling cases when your flow is empty, you should consider operators like next in the following manner:
transaction
.flatMap(it -> {
val user = userRepository.findById(it.userId)
})
.swithIfEmpty(Flux.defer(() -> Flux.just(badRequest())));
Actual solution
Also, I have noted that you created two sub-flows from the main transaction. Actually, following code will not be executed at all:
transaction.flatMap {
val user = userRepository.findById(it.userId)
// If it's empty, return badRequest()
}
and will be only executed the last one, which is returned from the method. That happens because you ain't subscribed using operator .subscribe(...).
The second point, you can't subscribe to the same request body more the one time (kind of limitation for WebClient's reponse). Thus you are required to share your request body in the next way, so completed example will be:
fun createTransaction(serverRequest: ServerRequest): Mono<ServerResponse> {
val transaction = serverRequest.body(BodyExtractors.toMono(Transaction::class.java)).cache()
transaction
.flatMap { userRepository.findById(it.userId) }
.flatMap { transaction.flatMap { transactionRepository.save(it) } }
.flatMap { ServerResponse.created(URI.create("/transaction/" + it.id)).build() }
.switchIfEmpty(transaction.flatMap { ServerResponse.badRequest().syncBody("missed User for transaction " + it.id) })
}
Or more simple case without sharing transaction flow but using Tuple:
fun createTransaction(serverRequest: ServerRequest): Mono<ServerResponse> {
val emptyUser = !User()
val transaction = serverRequest.body<Mono<Transaction>>(BodyExtractors.toMono(Transaction::class.java))
transaction
.flatMap { t ->
userRepository.findById(t.userId)
.map { Tuples.of(t, it) }
.defaultIfEmpty(Tuples.of(t, emptyUser))
}
.flatMap {
if (it.t2 != emptyUser) {
transactionRepository.save(it.t1)
.flatMap { ServerResponse.created(URI.create("/transaction/" + it.id)).build() }
} else {
ServerResponse.badRequest().syncBody("missed User for transaction " + it.t1.id)
}
}
}
You can check it using the Mono's provided method hasElement() which is analogous to Optional's isPresent(). The method definition is :
Mono<Boolean> hasElement()
for more details checkout : project reactor documentation
In case you have to perform some action based on this value you can further use switchIfEmpty() to provide with alternate publisher.
Let me start by saying I am a newbie on reactive (java) and on this forum.
I think you cannot really check in this code if a mono is empty because a mono represents code that will be executed later on, so in this code body you won't know yet if its is empty. Does that make sense?
I just wrote something similar in Java which seems to work (but not 100% this is the best approach either):
public Mono<ServerResponse> queryStore(ServerRequest request) {
Optional<String> postalCode = request.queryParam("postalCode");
Mono<ServerResponse> badQuery = ServerResponse.badRequest().build();
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
if (!postalCode.isPresent()) { return badQuery; }
Flux<Store> stores = this.repository
.getNearByStores(postalCode.get(), 5);
return ServerResponse.ok().contentType(APPLICATION_JSON)
.body(stores, Store.class)
.switchIfEmpty(notFound);
}
We can use switchIfEmpty method for this
Below example, I'm checking if the user exists with email if not then add it
userRepository.findByEmail(user.getEmail())
.switchIfEmpty(s -> {
user.setStatus("InActive");
String encodedPassword = DigestUtils.sha256Hex(user.getPassword());
user.setPassword(encodedPassword);
userRepository.save(user).subscribe();
s.onComplete();
}).then(Mono.just(user));
Use Mono with Optional:
return findExistingUserMono
.map(Optional::of)
.defaultIfEmpty(Optional.empty())
.flatMap(optionalUser -> {
if(optionalUser.isPresent()) {
return Mono.error('xxxx');
}
return this.userService.create(optionalUser.get());
});
This way it will always emit Optional value so that the stream will never break.