Skip to content

Commit

Permalink
Explicitly mock Instant.now to fix test flakiness
Browse files Browse the repository at this point in the history
Signed-off-by: Antoine MAZEAS <[email protected]>
  • Loading branch information
antoinemzs committed Dec 17, 2024
1 parent c9e3e73 commit e83485c
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mockStatic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand All @@ -24,11 +26,14 @@
import io.openbas.utils.mockUser.WithMockAdminUser;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.*;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
Expand All @@ -47,6 +52,7 @@ public class ExerciseApiStatusTest {
static Exercise CANCELED_EXERCISE;
static Inject SAVED_INJECT5;
static LessonsAnswer LESSON_ANSWER;
static Instant REFERENCE_TIME;

@Autowired private MockMvc mvc;

Expand Down Expand Up @@ -76,11 +82,13 @@ public class ExerciseApiStatusTest {

@BeforeEach
void beforeAll() {
Exercise scheduledExercise = ExerciseFixture.createDefaultAttackExercise();
Exercise runningExercise = ExerciseFixture.createRunningAttackExercise();
Exercise pausedExercise = ExerciseFixture.createPausedAttackExercise();
Exercise canceledExercise = ExerciseFixture.createCanceledAttackExercise();
Exercise finishedExercise = ExerciseFixture.createFinishedAttackExercise();
REFERENCE_TIME =
Instant.now(Clock.fixed(Instant.parse("2024-12-17T10:30:45Z"), ZoneId.of("UTC")));
Exercise scheduledExercise = ExerciseFixture.createDefaultAttackExercise(REFERENCE_TIME);
Exercise runningExercise = ExerciseFixture.createRunningAttackExercise(REFERENCE_TIME);
Exercise pausedExercise = ExerciseFixture.createPausedAttackExercise(REFERENCE_TIME);
Exercise canceledExercise = ExerciseFixture.createCanceledAttackExercise(REFERENCE_TIME);
Exercise finishedExercise = ExerciseFixture.createFinishedAttackExercise(REFERENCE_TIME);

InjectorContract injectorContract =
this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow();
Expand Down Expand Up @@ -176,28 +184,49 @@ void manualStartExerciseTest() throws Exception {
// -- PREPARE--
ExerciseUpdateStatusInput input = new ExerciseUpdateStatusInput();
input.setStatus(ExerciseStatus.RUNNING);

// -- EXECUTE --
String response =
mvc.perform(
put(EXERCISE_URI + "/" + SCHEDULED_EXERCISE.getId() + "/status")
.content(asJsonString(input))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();

// -- ASSERT --
Thread.sleep(2000);
List<ExecutableInject> injects = injectHelper.getInjectsToRun();
Instant nextMinute = now().truncatedTo(MINUTES).plus(1, MINUTES);
assertEquals(nextMinute.toString(), JsonPath.read(response, "$.exercise_start_date"));
assertEquals(
Arrays.asList(ExerciseStatus.CANCELED.name(), ExerciseStatus.PAUSED.name()),
JsonPath.read(response, "$.exercise_next_possible_status"));
assertEquals(1, injects.size());
Instant instantBeforeSetRun = REFERENCE_TIME;
Instant instantAfterSetRun = REFERENCE_TIME.plus(1, MINUTES);
Clock clock = Clock.fixed(REFERENCE_TIME, ZoneId.of("UTC"));

try (MockedStatic<Instant> mockedInstant = mockStatic(Instant.class, CALLS_REAL_METHODS)) {
try (MockedStatic<Clock> mockedClock = mockStatic(Clock.class, CALLS_REAL_METHODS)) {

mockedInstant.when(Instant::now).thenReturn(instantBeforeSetRun);
// we need this other mock because the production code
// inconsistently calls Instant.now() and LocalDateTime.now()
mockedClock.when(Clock::systemDefaultZone).thenReturn(clock);

// -- EXECUTE --
String response =
mvc.perform(
put(EXERCISE_URI + "/" + SCHEDULED_EXERCISE.getId() + "/status")
.content(asJsonString(input))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();

// -- ASSERT --

// NOTE: we are changing the time of Instant.now() here to fast forward in the future
mockedInstant.when(Instant::now).thenReturn(instantAfterSetRun);

List<ExecutableInject> injects = injectHelper.getInjectsToRun();
Instant nextMinute = instantBeforeSetRun.truncatedTo(MINUTES).plus(1, MINUTES);
assertEquals(nextMinute.toString(), JsonPath.read(response, "$.exercise_start_date"));
assertEquals(
Arrays.asList(ExerciseStatus.CANCELED.name(), ExerciseStatus.PAUSED.name()),
JsonPath.read(response, "$.exercise_next_possible_status"));
assertEquals(
1,
injects.stream()
.filter((ij) -> SCHEDULED_EXERCISE.getId().equals(ij.getExercise().getId()))
.toList()
.size());
}
}
}

@DisplayName("Check an exercise from canceled to scheduled")
Expand Down Expand Up @@ -263,37 +292,52 @@ void runExerciseAfterPauseTest() throws Exception {
// --PREPARE--
ExerciseUpdateStatusInput input = new ExerciseUpdateStatusInput();
input.setStatus(ExerciseStatus.RUNNING);

// --EXECUTE--
String response =
mvc.perform(
put(EXERCISE_URI + "/" + PAUSED_EXERCISE.getId() + "/status")
.content(asJsonString(input))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();

// --ASSERT--
Thread.sleep(2000);
List<ExecutableInject> injects = injectHelper.getInjectsToRun();
List<Pause> pauses = pauseRepository.findAllForExercise(PAUSED_EXERCISE.getId());
Optional<Exercise> exercise =
exerciseRepository.findById(JsonPath.read(response, "$.exercise_id"));
if (exercise.isPresent()) {
Exercise responseExercise = exercise.get();
assertEquals(Optional.empty(), responseExercise.getCurrentPause());
Instant instantBeforeSetRun = REFERENCE_TIME;
Instant instantAfterSetRun = REFERENCE_TIME.plus(1, MINUTES);
Clock clock = Clock.fixed(REFERENCE_TIME, ZoneId.of("UTC"));

try (MockedStatic<Instant> mockedInstant = mockStatic(Instant.class, CALLS_REAL_METHODS)) {
try (MockedStatic<Clock> mockedClock = mockStatic(Clock.class, CALLS_REAL_METHODS)) {

mockedInstant.when(Instant::now).thenReturn(instantBeforeSetRun);
// we need this other mock because the production code
// inconsistently calls Instant.now() and LocalDateTime.now()
mockedClock.when(Clock::systemDefaultZone).thenReturn(clock);

// --EXECUTE--
String response =
mvc.perform(
put(EXERCISE_URI + "/" + PAUSED_EXERCISE.getId() + "/status")
.content(asJsonString(input))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();

// --ASSERT--
// NOTE: we are changing the time of Instant.now() here to fast forward in the future
mockedInstant.when(Instant::now).thenReturn(instantAfterSetRun);

List<ExecutableInject> injects = injectHelper.getInjectsToRun();
List<Pause> pauses = pauseRepository.findAllForExercise(PAUSED_EXERCISE.getId());
Optional<Exercise> exercise =
exerciseRepository.findById(JsonPath.read(response, "$.exercise_id"));
if (exercise.isPresent()) {
Exercise responseExercise = exercise.get();
assertEquals(Optional.empty(), responseExercise.getCurrentPause());
}
assertEquals(1, pauses.size());
assertEquals(
Arrays.asList(ExerciseStatus.CANCELED.name(), ExerciseStatus.PAUSED.name()),
JsonPath.read(response, "$.exercise_next_possible_status"));
assertEquals(1, injects.size());

// --CLEAN--
pauseRepository.delete(pauses.getFirst());
}
}
assertEquals(1, pauses.size());
assertEquals(
Arrays.asList(ExerciseStatus.CANCELED.name(), ExerciseStatus.PAUSED.name()),
JsonPath.read(response, "$.exercise_next_possible_status"));
assertEquals(1, injects.size());

// --CLEAN--
pauseRepository.delete(pauses.getFirst());
}

@DisplayName("Check an exercise from running to paused")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.openbas.utils.fixtures;

import static java.time.Instant.now;
import static java.time.temporal.ChronoUnit.MINUTES;

import io.openbas.database.model.Exercise;
Expand Down Expand Up @@ -37,18 +36,26 @@ public static Exercise createDefaultCrisisExercise() {
}

public static Exercise createDefaultIncidentResponseExercise() {
return createDefaultIncidentResponseExercise(Instant.now());
}

public static Exercise createDefaultIncidentResponseExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setName("Incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
exercise.setSubtitle("An incident response exercise");
exercise.setFrom("[email protected]");
exercise.setCategory("incident-response");
exercise.setStatus(ExerciseStatus.SCHEDULED);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}

public static Exercise createDefaultAttackExercise() {
return createDefaultAttackExercise(Instant.now());
}

public static Exercise createDefaultAttackExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setName("Draft incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
Expand All @@ -57,11 +64,15 @@ public static Exercise createDefaultAttackExercise() {
exercise.setCategory("attack-scenario");
exercise.setMainFocus("incident-response");
exercise.setStatus(ExerciseStatus.SCHEDULED);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}

public static Exercise createRunningAttackExercise() {
return createRunningAttackExercise(Instant.now());
}

public static Exercise createRunningAttackExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setName("Draft incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
Expand All @@ -70,11 +81,15 @@ public static Exercise createRunningAttackExercise() {
exercise.setCategory("attack-scenario");
exercise.setMainFocus("incident-response");
exercise.setStatus(ExerciseStatus.RUNNING);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}

public static Exercise createCanceledAttackExercise() {
return createCanceledAttackExercise(Instant.now());
}

public static Exercise createCanceledAttackExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setName("Draft incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
Expand All @@ -83,11 +98,15 @@ public static Exercise createCanceledAttackExercise() {
exercise.setCategory("attack-scenario");
exercise.setMainFocus("incident-response");
exercise.setStatus(ExerciseStatus.CANCELED);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}

public static Exercise createFinishedAttackExercise() {
return createFinishedAttackExercise(Instant.now());
}

public static Exercise createFinishedAttackExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setName("Draft incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
Expand All @@ -96,21 +115,25 @@ public static Exercise createFinishedAttackExercise() {
exercise.setCategory("attack-scenario");
exercise.setMainFocus("incident-response");
exercise.setStatus(ExerciseStatus.FINISHED);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}

public static Exercise createPausedAttackExercise() {
return createPausedAttackExercise(Instant.now());
}

public static Exercise createPausedAttackExercise(Instant startTime) {
Exercise exercise = new Exercise();
exercise.setCurrentPause(now().truncatedTo(MINUTES).minus(1, MINUTES));
exercise.setCurrentPause(startTime.truncatedTo(MINUTES).minus(1, MINUTES));
exercise.setName("Draft incident response exercise");
exercise.setDescription("An incident response exercise for my enterprise");
exercise.setSubtitle("An incident response exercise");
exercise.setFrom("[email protected]");
exercise.setCategory("attack-scenario");
exercise.setMainFocus("incident-response");
exercise.setStatus(ExerciseStatus.PAUSED);
exercise.setStart(Instant.now());
exercise.setStart(startTime);
return exercise;
}
}

0 comments on commit e83485c

Please sign in to comment.