Skip to content

Commit

Permalink
fix: potential idempotency problem
Browse files Browse the repository at this point in the history
This is an attempt to verify institute response data and roll things
back more gracefully should anything go wrong after an Institute event
response.
  • Loading branch information
trev-dev committed Oct 19, 2023
1 parent b772d28 commit 57b8a2c
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public interface SagaDataMapper {
@Mapping(target = "payload", expression = "java(ca.bc.gov.educ.api.edx.utils.JsonUtil.getJsonStringFromObject(sagaData))")
@Mapping(target = "emailId", expression = "java(sagaData.getInitialEdxUser().isPresent() ? sagaData.getInitialEdxUser().get().getEmail() : \"\")")
@Mapping(target = "edxUserId", ignore = true)
@Mapping(target = "schoolID", ignore = true)
@Mapping(target = "districtID", ignore = true)
@Mapping(target = "schoolID", source = "sagaData.school.schoolId")
@Mapping(target = "districtID", source = "sagaData.school.districtId")
SagaEntity toModel(String sagaName, CreateSchoolSagaData sagaData) throws JsonProcessingException;

@Mapping(target = "status", ignore = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,6 @@ public void createSchool(Event event, SagaEntity saga, CreateSchoolSagaData saga

public void checkForInitialUser(Event event, SagaEntity saga, CreateSchoolSagaData sagaData)
throws JsonProcessingException {
School createdSchoolFromInstitute = JsonUtil.getJsonObjectFromString(School.class, event.getEventPayload());
CreateSchoolSagaData updatedSagaData = new CreateSchoolSagaData();
updatedSagaData.setSchool(createdSchoolFromInstitute);
updatedSagaData.setInitialEdxUser(sagaData.getInitialEdxUser());
saga.setPayload(JsonUtil.getJsonStringFromObject(updatedSagaData));
saga.setSchoolID(UUID.fromString(createdSchoolFromInstitute.getSchoolId()));

final SagaEventStatesEntity eventStates =
this.createEventState(saga, event.getEventType(), event.getEventOutcome(), event.getEventPayload());
saga.setSagaState(ONBOARD_INITIAL_USER.toString());
Expand All @@ -128,10 +121,12 @@ public void checkForInitialUser(Event event, SagaEntity saga, CreateSchoolSagaDa
final Event.EventBuilder nextEventBuilder = Event.builder()
.eventType(ONBOARD_INITIAL_USER)
.replyTo(this.getTopicToSubscribe())
.eventPayload(JsonUtil.getJsonStringFromObject(updatedSagaData))
.eventPayload(JsonUtil.getJsonStringFromObject(sagaData))
.sagaId(event.getSagaId());

if (updatedSagaData.getInitialEdxUser().isPresent()) {
if (sagaData.getInitialEdxUser().isPresent()) {
School createdSchoolFromInstitute = JsonUtil.getJsonObjectFromString(School.class, event.getEventPayload());
this.orchestratorService.attachInstituteSchoolToSaga(createdSchoolFromInstitute.getSchoolId(), saga);
nextEventBuilder.eventOutcome(INITIAL_USER_FOUND);
} else {
nextEventBuilder.eventOutcome(NO_INITIAL_USER_FOUND);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
import ca.bc.gov.educ.api.edx.orchestrator.EdxSchoolUserActivationInviteOrchestrator;
import ca.bc.gov.educ.api.edx.props.EmailProperties;
import ca.bc.gov.educ.api.edx.repository.EdxActivationCodeRepository;
import ca.bc.gov.educ.api.edx.rest.RestUtils;
import ca.bc.gov.educ.api.edx.struct.v1.CreateSchoolSagaData;
import ca.bc.gov.educ.api.edx.struct.v1.EdxPrimaryActivationCode;
import ca.bc.gov.educ.api.edx.struct.v1.EdxUser;
import ca.bc.gov.educ.api.edx.struct.v1.EdxUserSchoolActivationInviteSagaData;
import ca.bc.gov.educ.api.edx.struct.v1.EmailNotification;
import ca.bc.gov.educ.api.edx.struct.v1.School;
import ca.bc.gov.educ.api.edx.utils.JsonUtil;
import ca.bc.gov.educ.api.edx.utils.RequestUtil;
import lombok.AccessLevel;
import lombok.Getter;
import jakarta.persistence.EntityNotFoundException;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class CreateSchoolOrchestratorService {

private static final SagaDataMapper SAGA_DATA_MAPPER = SagaDataMapper.mapper;
Expand All @@ -43,29 +46,49 @@ public class CreateSchoolOrchestratorService {

private final EdxActivationCodeRepository edxActivationCodeRepository;

@Getter(AccessLevel.PRIVATE)
private final EdxUsersService service;

private final EmailProperties emailProperties;
private final EmailNotificationService emailNotificationService;
private final RestUtils restUtils;

public CreateSchoolOrchestratorService(
SagaService sagaService,
EdxActivationCodeRepository edxActivationCodeRepository,
EdxUsersService service,
EmailProperties emailProperties,
EmailNotificationService emailNotificationService,
EdxSchoolUserActivationInviteOrchestrator activationInviteOrchestrator
EdxSchoolUserActivationInviteOrchestrator activationInviteOrchestrator,
RestUtils restUtils
) {
this.sagaService = sagaService;
this.edxActivationCodeRepository = edxActivationCodeRepository;
this.service = service;
this.emailProperties = emailProperties;
this.emailNotificationService = emailNotificationService;
this.activationInviteOrchestrator = activationInviteOrchestrator;
this.restUtils = restUtils;
}

@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void attachInstituteSchoolToSaga(String schoolId, SagaEntity saga)
throws JsonProcessingException {
List<School> result = restUtils.getSchoolById(saga.getSagaId(), schoolId);

if (result.isEmpty()) {
log.error("Could find School in Institute API :: {}", saga.getSagaId());
throw new EntityNotFoundException("School entity not found");
}

School school = result.get(0);
saga.setSchoolID(UUID.fromString(school.getSchoolId()));
CreateSchoolSagaData payload = JsonUtil.getJsonObjectFromString(CreateSchoolSagaData.class, saga.getPayload());
payload.setSchool(school);
saga.setPayload(JsonUtil.getJsonStringFromObject(payload));
sagaService.updateAttachedEntityDuringSagaProcess(saga);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPrimaryActivationCode(CreateSchoolSagaData sagaData) {
EdxPrimaryActivationCode edxPrimaryActivationCode = new EdxPrimaryActivationCode();
School school = sagaData.getSchool();
Expand All @@ -76,7 +99,7 @@ public void createPrimaryActivationCode(CreateSchoolSagaData sagaData) {
service.generateOrRegeneratePrimaryEdxActivationCode(SCHOOL, school.getSchoolId(), edxPrimaryActivationCode);
}

@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendPrimaryActivationCodeNotification(CreateSchoolSagaData sagaData) {
EdxUser user = sagaData.getInitialEdxUser().orElseThrow();
School school = sagaData.getSchool();
Expand All @@ -102,7 +125,7 @@ public void sendPrimaryActivationCodeNotification(CreateSchoolSagaData sagaData)
emailNotificationService.sendEmail(emailNotification);
}

@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void startEdxSchoolUserInviteSaga(CreateSchoolSagaData sagaData) {
EdxUserSchoolActivationInviteSagaData inviteSagaData = new EdxUserSchoolActivationInviteSagaData();
EdxUser user = sagaData.getInitialEdxUser().orElseThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.verify;
import static org.mockito.ArgumentMatchers.any;

import java.io.IOException;
import java.time.LocalDateTime;
Expand All @@ -31,6 +33,7 @@
import java.util.concurrent.TimeoutException;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
Expand Down Expand Up @@ -100,19 +103,29 @@ class CreateSchoolOrchestratorTest extends BaseSagaControllerTest {
@Autowired
EdxActivationCodeRepository edxActivationCodeRepository;

private School mockSchool;
private School mockInstituteSchool;

@Captor
ArgumentCaptor<byte[]> eventCaptor;

private static final SagaDataMapper SAGA_DATA_MAPPER = SagaDataMapper.mapper;

@BeforeEach
public void before() {
this.mockSchool = createMockSchool();
this.mockInstituteSchool = createMockSchoolFromInstitute(this.mockSchool);
doReturn(List.of(this.mockInstituteSchool)).when(this.restUtils).getSchoolById(any(), any());
}

@AfterEach
public void after() {
tearDown();
}

@Test
void testCreateSchool_GivenEventAndSagaData_shouldPostEventToInstituteApi() throws JsonProcessingException {
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.createMockSchool());
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockSchool);
final SagaEntity saga = saveMockSaga(mockData);

final int invocations = mockingDetails(messagePublisher).getInvocations().size();
Expand Down Expand Up @@ -142,8 +155,7 @@ void testCreateSchool_GivenEventAndSagaData_shouldPostEventToInstituteApi() thro
@Test
void testCheckForIntitialUser_GivenAnInitialUser_sagaShouldOnboardUser()
throws JsonProcessingException, IOException, TimeoutException, InterruptedException {
final School mockSchoolFromInstitute = this.createMockSchool();
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(mockSchoolFromInstitute);
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockSchool);
mockData.setInitialEdxUser(createMockInitialUser());
SagaEntity saga = saveMockSaga(mockData);

Expand All @@ -152,7 +164,7 @@ void testCheckForIntitialUser_GivenAnInitialUser_sagaShouldOnboardUser()
.eventType(CREATE_SCHOOL)
.eventOutcome(SCHOOL_CREATED)
.sagaId(saga.getSagaId())
.eventPayload(getJsonString(mockSchoolFromInstitute))
.eventPayload(getJsonString(this.mockInstituteSchool))
.build();
orchestrator.handleEvent(event);

Expand All @@ -171,16 +183,15 @@ void testCheckForIntitialUser_GivenAnInitialUser_sagaShouldOnboardUser()
@Test
void testCheckForIntitialUser_GivenNoIntialUser_sagaShouldBeCompleted()
throws JsonProcessingException, IOException, TimeoutException, InterruptedException {
final School mockSchoolFromInstitute = this.createMockSchool();
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(mockSchoolFromInstitute);
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockSchool);
SagaEntity saga = saveMockSaga(mockData);

final int invocations = mockingDetails(messagePublisher).getInvocations().size();
final Event event = Event.builder()
.eventType(CREATE_SCHOOL)
.eventOutcome(SCHOOL_CREATED)
.sagaId(saga.getSagaId())
.eventPayload(getJsonString(mockSchoolFromInstitute))
.eventPayload(getJsonString(this.mockInstituteSchool))
.build();
orchestrator.handleEvent(event);

Expand All @@ -207,8 +218,7 @@ void testCheckForIntitialUser_GivenNoIntialUser_sagaShouldBeCompleted()
@Test
void testCreatePrimaryCode_GivenAnInitialUserAndSchool_sagaShouldCreatePrimarySchoolCode()
throws TimeoutException, IOException, InterruptedException {
final School mockSchoolFromInstitute = this.createMockSchool();
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(mockSchoolFromInstitute);
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockInstituteSchool);
mockData.setInitialEdxUser(createMockInitialUser());
SagaEntity saga = saveMockSaga(mockData);

Expand All @@ -217,7 +227,7 @@ void testCreatePrimaryCode_GivenAnInitialUserAndSchool_sagaShouldCreatePrimarySc
.eventType(ONBOARD_INITIAL_USER)
.eventOutcome(INITIAL_USER_FOUND)
.sagaId(saga.getSagaId())
.eventPayload(getJsonString(mockSchoolFromInstitute))
.eventPayload(getJsonString(mockData))
.build();
orchestrator.handleEvent(event);

Expand All @@ -241,8 +251,7 @@ void testCreatePrimaryCode_GivenAnInitialUserAndSchool_sagaShouldCreatePrimarySc
@Test
void testSendPrimaryCode_GivenAnInitialUser_School_AndPrimaryCode_sagaShouldSendAPrimaryCodeToUser()
throws TimeoutException, IOException, InterruptedException {
final School mockSchoolFromInstitute = this.createMockSchool();
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(mockSchoolFromInstitute);
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockInstituteSchool);
mockData.setInitialEdxUser(createMockInitialUser());
SagaEntity saga = saveMockSaga(mockData);

Expand All @@ -251,7 +260,7 @@ void testSendPrimaryCode_GivenAnInitialUser_School_AndPrimaryCode_sagaShouldSend
.eventType(ONBOARD_INITIAL_USER)
.eventOutcome(INITIAL_USER_FOUND)
.sagaId(saga.getSagaId())
.eventPayload(getJsonString(mockSchoolFromInstitute))
.eventPayload(getJsonString(mockData))
.build();
orchestrator.handleEvent(event);

Expand All @@ -272,7 +281,7 @@ void testSendPrimaryCode_GivenAnInitialUser_School_AndPrimaryCode_sagaShouldSend

@Test
void testInviteInitialUser_GivenEventAndSaga_sagaShouldStartInviteSaga() throws IOException, InterruptedException, TimeoutException {
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(createMockSchool());
final CreateSchoolSagaData mockData = createMockCreateSchoolSagaData(this.mockInstituteSchool);
mockData.setInitialEdxUser(createMockInitialUser());
SagaEntity saga = saveMockSaga(mockData);
createRoleAndPermissionData(edxPermissionRepository, edxRoleRepository);
Expand Down Expand Up @@ -328,12 +337,18 @@ private School createMockSchool() {
school.setWebsite("abc@sd99.edu");
school.setCreateUser("TEST");
school.setUpdateUser("TEST");
school.setSchoolId(UUID.randomUUID().toString());
school.setDistrictId(UUID.randomUUID().toString());

return school;
}

private School createMockSchoolFromInstitute(School school) {
School updatedSchool = this.createMockSchool();
updatedSchool.setSchoolId(UUID.randomUUID().toString());
updatedSchool.setDistrictId(school.getDistrictId());
return updatedSchool;
}

private Optional<EdxUser> createMockInitialUser() {
EdxUser mockUser = new EdxUser();
mockUser.setEmail("test@gov.bc.ca");
Expand Down

0 comments on commit 57b8a2c

Please sign in to comment.