So I have the small app that I am working on it has 3 entities Device, StakeLimit (which has Device Foreign key) and TicketMessage (this one is irrelevant for the problem), and it has couple of services, controllers etc. The problem is in StakeLimitService specifically in StakeLimitServiceTest unit test that I am writing. The problem is in method that I am testing it keeps throwing my custom exception DeviceNotFoundException which is thrown when device by provided id is not found in repository and the problem there is it should not throw exception yet it does. I tried everything to get rid of it including using chatGPT in hope of finding the solution and I only got it to work with one solution which makes no sense to me why it is working yet my original one is not (this I provided at the bottom).
This is my code: Device:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "Device")
@Table(name = "devices")
public class Device {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(
name = "id",
updatable = false
)
private UUID id;
@Column(name = "blocked")
private boolean blocked;
@Column(name = "restriction_expires")
private boolean restrictionExpires;
@Column(name = "restriction_expires_at")
private LocalDateTime restrictionExpiresAt;
}
DeviceRepository:
@Repository
public interface DeviceRepository extends JpaRepository<Device, UUID> {
Device findDeviceById(UUID id);
}
StakeLimit:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity(name = "StakeLimit")
@Table(name = "stake_limits")
public class StakeLimit {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(
name = "id",
updatable = false
)
private UUID id;
@ManyToOne
@JoinColumn(
name = "device_id",
nullable = false,
referencedColumnName = "id",
foreignKey = @ForeignKey(
name = "device_id_fk"
)
)
private Device device;
@Column(
name = "time_duration",
nullable = false
)
private Integer timeDuration;
@Column(
name = "stake_limit",
nullable = false
)
private Double stakeLimit;
@Column(
name = "hot_amount_pctg",
nullable = false
)
private Integer hotAmountPctg;
@Column(
name = "restr_expiry",
nullable = false
)
private Integer restrExpiry;
}
StakeLimitRepository:
public interface StakeLimitRepository extends JpaRepository<StakeLimit, UUID> {
boolean existsByDevice(Device device);
StakeLimit findByDevice(Device device);
}
StakeLimitRequest:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StakeLimitRequest {
//Using String instead of UUID because I could not get my custom validation to work
@NotEmpty(message = "DeviceId field can't be empty")
private String deviceId;
@Min(value = 300, message = "Minimum time duration is 5 minutes (300 seconds)")
@Max(value = 86400, message = "Maximum time duration is 24 hours (86400 seconds)")
private Integer timeDuration;
@Min(value = 1, message = "Minimum stake limit is 1")
@Max(value = 10000000, message = "Maximum stake limit is 10000000")
private Double stakeLimit;
@Min(value = 1, message = "Minimum hot amount percentage is 1")
@Max(value = 100, message = "Maximum hot amount percentage is 100")
private Integer hotAmountPctg;
@Min(value = 0, message = "Minimum restriction expiry is 1 minute (60 seconds)" +
" while maximum is 0 seconds (it never expires)")
private Integer restrExpiry;
}
StakeLimitResponse:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StakeLimitResponse {
private Integer timeDuration;
private Double stakeLimit;
private Integer hotAmountPctg;
private Integer restrExpiry;
}
StakeLimitService:
@Service
@RequiredArgsConstructor
public class StakeLimitService {
private final StakeLimitRepository stakeLimitRepository;
private final DeviceRepository deviceRepository;
public StakeLimitResponse getStakeLimit(String deviceId) {
//method for checking if provided deviceId is actually UUID (for more info
//check comments above isUuidValid method)
UUID deviceUuid = isUuidValid(deviceId);
//then check if device by that id exists in our devices table
if (!deviceRepository.existsById(deviceUuid)) {
//if it does not throw exp
throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
}
//else check if that device contains stake limits
if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
//if not throw exp
throw new StakeLimitNotFoundException("Stake limit with device " + deviceUuid + " id not found");
}
//else get device and then get our stake limit using that device
var device = deviceRepository.findDeviceById(deviceUuid);
var stakeLimit = stakeLimitRepository.findByDevice(device);
//and finally build our response and return it to user
return StakeLimitResponse.builder()
.timeDuration(stakeLimit.getTimeDuration())
.stakeLimit(stakeLimit.getStakeLimit())
.hotAmountPctg(stakeLimit.getHotAmountPctg())
.restrExpiry(stakeLimit.getRestrExpiry())
.build();
}
public StakeLimitResponse addStakeLimit(StakeLimitRequest request) {
//same as above
UUID deviceId = isUuidValid(request.getDeviceId());
//Could also make it if deviceId does not exist, create device with that id
if (!deviceRepository.existsById(deviceId)) {
throw new DeviceNotFoundException("Device with " + deviceId + " id not found");
}
if (stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceId))) {
throw new DeviceAlreadyExistsException(
"Stake limit with device " + deviceId + " id already exists"
);
}
var device = deviceRepository.findDeviceById(deviceId);
//when all checks are completed we finally check if restriction is set to
//expire or not and set it to our device and save it to our db again
device.setRestrictionExpires(request.getRestrExpiry() != 0);
deviceRepository.save(device);
//then we build our stake limit and save it to db
var stakeLimit = StakeLimit.builder()
.device(device)
.timeDuration(request.getTimeDuration())
.stakeLimit(request.getStakeLimit())
.hotAmountPctg(request.getHotAmountPctg())
.restrExpiry(request.getRestrExpiry())
.build();
stakeLimitRepository.save(stakeLimit);
//finally build our response and return it
return StakeLimitResponse.builder()
.timeDuration(stakeLimit.getTimeDuration())
.stakeLimit(stakeLimit.getStakeLimit())
.hotAmountPctg(stakeLimit.getHotAmountPctg())
.restrExpiry(stakeLimit.getRestrExpiry())
.build();
}
@Transactional
public StakeLimitResponse changeStakeLimit(
String deviceId,
Integer timeDuration,
Double stakeLimit,
Integer hotAmountPctg,
Integer restrExpiry
) {
//same checks as the above
UUID deviceUuid = isUuidValid(deviceId);
if (!deviceRepository.existsById(deviceUuid)) {
throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
}
if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
throw new StakeLimitNotFoundException("Stake limit for device with id " + deviceUuid + " not found");
}
var device = deviceRepository.findDeviceById(deviceUuid);
var stakeLimitDB = stakeLimitRepository.findByDevice(device);
//then we check which fields user actually sent (decided to change) and change them
if (timeDuration != null && !timeDuration.equals(stakeLimitDB.getTimeDuration())) {
stakeLimitDB.setTimeDuration(timeDuration);
}
if (stakeLimit != null && !stakeLimit.equals(stakeLimitDB.getStakeLimit())) {
stakeLimitDB.setStakeLimit(stakeLimit);
}
if (hotAmountPctg != null && !hotAmountPctg.equals(stakeLimitDB.getHotAmountPctg())) {
stakeLimitDB.setHotAmountPctg(hotAmountPctg);
}
if (restrExpiry != null && !restrExpiry.equals(stakeLimitDB.getRestrExpiry())) {
device.setRestrictionExpires(restrExpiry != 0);
deviceRepository.save(device);
stakeLimitDB.setRestrExpiry(restrExpiry);
}
stakeLimitRepository.save(stakeLimitDB);
return StakeLimitResponse.builder()
.timeDuration(stakeLimitDB.getTimeDuration())
.stakeLimit(stakeLimitDB.getStakeLimit())
.hotAmountPctg(stakeLimitDB.getHotAmountPctg())
.restrExpiry(stakeLimitDB.getRestrExpiry())
.build();
}
//method used for checking if provided UUID is correct
//because I could not get custom validation to work properly I switched all requests
//to take String instead of pure UUID, and then i check if that String matches
//UUID pattern, if it does not match pattern throw custom exception else
//transform provided String into actual UUID
private UUID isUuidValid(String uuid) {
final String uuid_pattern = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$";
if (!uuid.matches(uuid_pattern)) {
throw new UuidNotValidException(uuid + " UUID not valid");
}
return UUID.fromString(uuid);
}
}
StakeLimitServiceTest:
@ExtendWith(MockitoExtension.class)
class StakeLimitServiceTest {
@Mock
private StakeLimitRepository stakeLimitRepository;
@Mock
private DeviceRepository deviceRepository;
@InjectMocks
private StakeLimitService stakeLimitService;
private StakeLimit stakeLimit;
private StakeLimitRequest stakeLimitRequest;
private StakeLimitResponse stakeLimitResponse;
private Device device;
@BeforeEach
void setUp() {
device = Device.builder().id(UUID.fromString("799de2ee-13c2-40a1-8230-d7318de97925")).build();
stakeLimit = StakeLimit.builder()
.device(device)
.timeDuration(1800)
.stakeLimit(999.0)
.hotAmountPctg(80)
.restrExpiry(300)
.build();
stakeLimitRequest = StakeLimitRequest.builder()
.deviceId(device.getId().toString())
.timeDuration(1800)
.stakeLimit(999.0)
.hotAmountPctg(80)
.restrExpiry(300)
.build();
stakeLimitResponse = StakeLimitResponse.builder()
.timeDuration(1800)
.stakeLimit(999.0)
.hotAmountPctg(80)
.restrExpiry(300)
.build();
}
@Test
void Should_ReturnStakeLimitResponse_When_GetStakeLimit() {
// when
when(deviceRepository.findDeviceById(device.getId())).thenReturn(device);
when(stakeLimitRepository.findByDevice(device)).thenReturn(stakeLimit);
StakeLimitResponse response = stakeLimitService.getStakeLimit(device.getId().toString());
// then
assertThat(response).isNotNull();
verify(deviceRepository).findDeviceById(any());
verify(stakeLimitRepository).findByDevice(any());
}
@Disabled
@Test
void Should_CreateStakeLimitAndReturnStakeLimitResponse_When_AddStakeLimit() {
// given
stakeLimitRequest.setDeviceId(device.getId().toString());
// when
when(deviceRepository.findDeviceById(device.getId())).thenReturn(device);
when(stakeLimitRepository.save(any(StakeLimit.class))).thenReturn(stakeLimit);
StakeLimitResponse savedStakeLimit = stakeLimitService.addStakeLimit(stakeLimitRequest);
// then
assertThat(savedStakeLimit).isNotNull();
verify(deviceRepository).findDeviceById(any());
verify(stakeLimitRepository).save(any());
}
@Disabled
@Test
void Should_ChangeStakeLimitAndReturnStakeLimitResponse_When_ChangeStakeLimit() {
}
}
Also for some reason when I just try to create new device
Device device = new Device()
my device has null for id rather than auto generating UUID and that is also why I am building the device myself and passing it UUID.
The only thing that worked is when I switched
UUID deviceUuid = isUuidValid(deviceId);
if (!deviceRepository.existsById(deviceUuid)) {
throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
}
if (!stakeLimitRepository.existsByDevice(deviceRepository.findDeviceById(deviceUuid))) {
throw new StakeLimitNotFoundException("Stake limit with device " + deviceUuid + " id not found");
}
var device = deviceRepository.findDeviceById(deviceUuid);
var stakeLimit = stakeLimitRepository.findByDevice(device);
For
Device device = deviceRepository.findDeviceById(deviceId);
UUID deviceUuid = isUuidValid(deviceId);
Device device = deviceRepository.findDeviceById(deviceUuid);
if (device == null) {
throw new DeviceNotFoundException("Device with " + deviceUuid + " id not found");
}
StakeLimit stakeLimit = stakeLimitRepository.findByDevice(device);
if (stakeLimit == null) {
throw new StakeLimitNotFoundException("Stake limit not found for device " + deviceUuid);
}
which chatGPT suggested and I have no clue why that solution works yet mine does not when they basically do the same thing.
So just as @Akashtiwari said I wasn't mocking two methods I was using in service those being existsById and existsByDevice so adding those two mocks fixed the problem. when(deviceRepository.existsById(device.getId())).thenReturn(true);
and when(stakeLimitRepository.existsByDevice(device)).thenReturn(true);
fixed the issue.