1 package de.dlr.shepard.data.timeseries.services;
2
3 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4 import static org.junit.jupiter.api.Assertions.assertEquals;
5 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
6 import static org.junit.jupiter.api.Assertions.assertNotNull;
7 import static org.junit.jupiter.api.Assertions.assertThrows;
8 import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
9 import static org.junit.jupiter.api.Assertions.assertTrue;
10 import static org.junit.jupiter.api.Assertions.fail;
11 import static org.mockito.Mockito.mock;
12 import static org.mockito.Mockito.when;
13
14 import de.dlr.shepard.auth.security.AuthenticationContext;
15 import de.dlr.shepard.auth.users.entities.User;
16 import de.dlr.shepard.auth.users.services.UserService;
17 import de.dlr.shepard.common.exceptions.InvalidAuthException;
18 import de.dlr.shepard.common.exceptions.InvalidBodyException;
19 import de.dlr.shepard.common.exceptions.InvalidPathException;
20 import de.dlr.shepard.data.timeseries.TimeseriesTestDataGenerator;
21 import de.dlr.shepard.data.timeseries.io.TimeseriesContainerIO;
22 import de.dlr.shepard.data.timeseries.model.TimeseriesContainer;
23 import de.dlr.shepard.data.timeseries.model.TimeseriesDataPoint;
24 import de.dlr.shepard.data.timeseries.model.TimeseriesDataPointsQueryParams;
25 import de.dlr.shepard.data.timeseries.model.TimeseriesTuple;
26 import io.quarkus.test.InjectMock;
27 import io.quarkus.test.junit.QuarkusTest;
28 import jakarta.inject.Inject;
29 import jakarta.transaction.Transactional;
30 import jakarta.ws.rs.core.Response.Status;
31 import java.time.Instant;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.List;
35 import java.util.NoSuchElementException;
36 import java.util.Optional;
37 import org.eclipse.microprofile.config.Config;
38 import org.eclipse.microprofile.config.ConfigProvider;
39 import org.junit.jupiter.api.BeforeEach;
40 import org.junit.jupiter.api.Test;
41 import org.mockito.Mockito;
42
43 @QuarkusTest
44 public class TimeseriesServiceTest {
45
46 @Inject
47 TimeseriesService timeseriesService;
48
49 @Inject
50 TimeseriesContainerService timeseriesContainerService;
51
52 @InjectMock
53 UserService userService;
54
55 @InjectMock
56 AuthenticationContext authenticationContext;
57
58 private final String containerName = "TimeseriesServiceTestContainer";
59 private final long startDate = InstantHelper.fromGermanDate("01.01.2024").toNano();
60 private final long endDate = InstantHelper.now().addHours(1).toNano();
61
62 private TimeseriesContainer container;
63
64 @BeforeEach
65 public void setUpEach() {
66 User user = new User("Testuser");
67 when(userService.getCurrentUser()).thenReturn(user);
68 when(authenticationContext.getCurrentUserName()).thenReturn(user.getUsername());
69
70 TimeseriesContainerIO containerIO = new TimeseriesContainerIO();
71 containerIO.setName(containerName);
72
73 container = timeseriesContainerService.createContainer(containerIO);
74 }
75
76 @Test
77 @Transactional
78 public void saveDataPoints_addDoubleValue_success() {
79 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("measurement");
80 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
81 var point = TimeseriesTestDataGenerator.generateDataPointDouble(123.456);
82 dataPoints.add(point);
83
84 var created = this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
85 assertNotNull(created);
86 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
87 startDate,
88 endDate,
89 null,
90 null,
91 null
92 );
93 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
94 assertNotNull(actual);
95 assertEquals(1, actual.size());
96 TimeseriesDataPoint actualPoint = actual.getFirst();
97 assertInstanceOf(Double.class, actualPoint.getValue(), "DataPoint value must be a double");
98 assertEquals(point.getTimestamp(), actualPoint.getTimestamp(), "DataPoint timestamp must be taken over");
99 }
100
101 @Test
102 @Transactional
103 public void saveDataPoints_addBooleanValue_success() {
104 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("measurement");
105 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
106 var point = TimeseriesTestDataGenerator.generateDataPointBoolean(true);
107 dataPoints.add(point);
108
109 var created = this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
110 assertNotNull(created);
111 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
112 startDate,
113 endDate,
114 null,
115 null,
116 null
117 );
118 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
119 assertNotNull(actual);
120 assertEquals(1, actual.size());
121 TimeseriesDataPoint actualPoint = actual.getFirst();
122 assertInstanceOf(Boolean.class, actualPoint.getValue(), "DataPoint value must be a boolean");
123 assertEquals(point.getTimestamp(), actualPoint.getTimestamp(), "DataPoint timestamp must be taken over");
124 }
125
126 @Test
127 @Transactional
128 public void saveDataPoints_addStringValue_success() {
129 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("measurement");
130 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
131 var point = TimeseriesTestDataGenerator.generateDataPointString("Hello World");
132 dataPoints.add(point);
133
134 var created = this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
135 assertNotNull(created);
136 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
137 startDate,
138 endDate,
139 null,
140 null,
141 null
142 );
143 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
144 assertNotNull(actual);
145 assertEquals(1, actual.size());
146 TimeseriesDataPoint actualPoint = actual.getFirst();
147 assertInstanceOf(String.class, actualPoint.getValue(), "DataPoint value must be a string");
148 assertEquals(point.getTimestamp(), actualPoint.getTimestamp(), "DataPoint timestamp must be taken over");
149 }
150
151 @Test
152 @Transactional
153 public void saveDataPoints_addIntegerValue_success() {
154 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("measurement");
155 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
156 var point = TimeseriesTestDataGenerator.generateDataPointInteger(42);
157 dataPoints.add(point);
158
159 var created = this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
160 assertNotNull(created);
161 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
162 startDate,
163 endDate,
164 null,
165 null,
166 null
167 );
168 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
169 assertNotNull(actual);
170 assertEquals(1, actual.size());
171 TimeseriesDataPoint actualPoint = actual.getFirst();
172 assertInstanceOf(Long.class, actualPoint.getValue(), "DataPoint value must be a long");
173 assertEquals(point.getTimestamp(), actualPoint.getTimestamp(), "DataPoint timestamp must be taken over");
174 }
175
176 @Test
177 @Transactional
178 public void saveDataPoints_toExistingTimeseries_success() {
179 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("temperature");
180 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
181 List.of(TimeseriesTestDataGenerator.generateDataPointDouble(22.1))
182 );
183
184 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
185
186 List<TimeseriesDataPoint> morePoints = new ArrayList<>(
187 List.of(TimeseriesTestDataGenerator.generateDataPointDouble(22.2))
188 );
189
190 this.timeseriesService.saveDataPoints(container.getId(), timeseries, morePoints);
191 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
192 startDate,
193 endDate,
194 null,
195 null,
196 null
197 );
198 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
199 assertEquals(2, actual.size());
200 }
201
202 @Test
203 @Transactional
204 public void saveDataPoints_requiredFieldsMissing_throwsException() {
205 var timeseries = new TimeseriesTuple("", "", "", "", "");
206 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
207 var point = TimeseriesTestDataGenerator.generateDataPointInteger(5);
208 dataPoints.add(point);
209
210 InvalidBodyException thrown = assertThrowsExactly(InvalidBodyException.class, () ->
211 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints)
212 );
213
214 assertEquals(Status.BAD_REQUEST.getStatusCode(), thrown.getResponse().getStatus());
215 }
216
217 @Test
218 @Transactional
219 public void saveDataPoints_addDataPointToExistingTimeseriesWithDifferentType_throwsException() {
220 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("temperature");
221
222 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
223 var point = TimeseriesTestDataGenerator.generateDataPointDouble(22.3);
224 dataPoints.add(point);
225 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
226
227 List<TimeseriesDataPoint> otherDataPoints = new ArrayList<>();
228 var pointWithDifferentType = TimeseriesTestDataGenerator.generateDataPointInteger(20);
229 otherDataPoints.add(pointWithDifferentType);
230
231 InvalidBodyException thrown = assertThrowsExactly(InvalidBodyException.class, () ->
232 this.timeseriesService.saveDataPoints(container.getId(), timeseries, otherDataPoints)
233 );
234
235 assertEquals(Status.BAD_REQUEST.getStatusCode(), thrown.getResponse().getStatus());
236 }
237
238 @Test
239 @Transactional
240 public void saveDataPoints_addDataPointToExistingTimeseriesWithDifferentType_autoConversion() {
241 try (var configProviderMock = Mockito.mockStatic(ConfigProvider.class)) {
242 var config = mock(Config.class);
243 configProviderMock.when(ConfigProvider::getConfig).thenReturn(config);
244 when(config.getOptionalValue("shepard.autoconvert-int", Boolean.class)).thenReturn(Optional.of(true));
245
246 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("temperature");
247
248 List<TimeseriesDataPoint> dataPoints = new ArrayList<>();
249 var point = TimeseriesTestDataGenerator.generateDataPointDouble(22.3);
250 dataPoints.add(point);
251 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
252
253 List<TimeseriesDataPoint> otherDataPoints = new ArrayList<>();
254 var pointWithDifferentType = TimeseriesTestDataGenerator.generateDataPointInteger(20);
255 otherDataPoints.add(pointWithDifferentType);
256
257 assertDoesNotThrow(() -> {
258 this.timeseriesService.saveDataPoints(container.getId(), timeseries, otherDataPoints);
259 });
260
261 var queryParams = new TimeseriesDataPointsQueryParams(
262 0L,
263 Instant.now().toEpochMilli() * 1_000_000,
264 null,
265 null,
266 null
267 );
268 var storedPoints = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
269
270 assertEquals(2, storedPoints.size());
271
272 var storedValues = storedPoints.stream().map(TimeseriesDataPoint::getValue).toList();
273
274 storedValues.forEach(item -> assertInstanceOf(Double.class, item));
275
276 List<Double> expectedValues = Arrays.asList(22.3, 20.0);
277
278 assertTrue(storedValues.containsAll(expectedValues));
279 }
280 }
281
282 @Test
283 @Transactional
284 public void getTimeseriesAvailable_timeseriesExists_returnsTimeseries() {
285 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("temperature");
286 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
287 List.of(TimeseriesTestDataGenerator.generateDataPointDouble(22.1))
288 );
289
290 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
291
292 var actual = this.timeseriesService.getTimeseriesAvailable(container.getId()).toList();
293 assertEquals(1, actual.size());
294 assertEquals("temperature", actual.getFirst().getTimeseriesTuple().getMeasurement());
295 }
296
297 @Test
298 public void getTimeseriesById_timeseriesDoesNotExist_throwsNoSuchElementException() {
299 var nonExistingTimeseriesId = -1L;
300
301 assertThrowsExactly(NoSuchElementException.class, () ->
302 this.timeseriesService.getTimeseriesById(nonExistingTimeseriesId)
303 );
304 }
305
306 @Test
307 public void getTimeseriesById_timeseriesExists_containerNotReadable_throwsInvalidAuthException() {
308
309 User user1 = new User("u1");
310 when(userService.getCurrentUser()).thenReturn(user1);
311 when(authenticationContext.getCurrentUserName()).thenReturn(user1.getUsername());
312 TimeseriesContainerIO containerIO = new TimeseriesContainerIO();
313 containerIO.setName(containerName);
314 var privateContainerId = timeseriesContainerService.createContainer(containerIO).getId();
315 var nonAccessibleTimeseriesId = timeseriesService
316 .saveDataPoints(
317 privateContainerId,
318 new TimeseriesTuple("m", "d", "l", "s", "f"),
319 List.of(new TimeseriesDataPoint(1, 1))
320 )
321 .getTimeseriesId();
322
323 User user2 = new User("u2");
324 when(userService.getCurrentUser()).thenReturn(user2);
325 when(authenticationContext.getCurrentUserName()).thenReturn(user2.getUsername());
326
327
328 assertThrows(InvalidAuthException.class, () -> this.timeseriesService.getTimeseriesById(nonAccessibleTimeseriesId));
329 }
330
331 @Test
332 public void getTimeseries_timeseriesDoesNotExist_returnsEmpty() {
333 TimeseriesTuple nonExistingTimeseries = new TimeseriesTuple(
334 "nonExisting",
335 "nonExisting",
336 "nonExisting",
337 "nonExisting",
338 "nonExisting"
339 );
340
341 assertEquals(Optional.empty(), this.timeseriesService.getTimeseries(container.getId(), nonExistingTimeseries));
342 }
343
344 @Test
345 @Transactional
346 public void getTimeseriesAvailable_containerDoesNotExist_throwsNotFoundException() {
347 int nonExistingContainerId = -1;
348
349 assertThrowsExactly(InvalidPathException.class, () ->
350 this.timeseriesService.getTimeseriesAvailable(nonExistingContainerId)
351 );
352 }
353
354 @Test
355 @Transactional
356 public void getDataPointsByTimeseries_forGivenDuration_returnsAll() {
357 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("humidity");
358 var start = InstantHelper.now().addDays(-4).toNano();
359 var end = InstantHelper.now().addDays(-2).toNano();
360
361 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
362 List.of(
363 TimeseriesTestDataGenerator.generateDataPointInteger(start, 70),
364 TimeseriesTestDataGenerator.generateDataPointInteger(start + 1000, 80),
365 TimeseriesTestDataGenerator.generateDataPointInteger(start + 100000, 65),
366 TimeseriesTestDataGenerator.generateDataPointInteger(end - 10000, 72),
367 TimeseriesTestDataGenerator.generateDataPointInteger(end, 88)
368 )
369 );
370
371 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
372
373 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(start, end, null, null, null);
374
375 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
376
377 assertEquals(dataPoints.size(), actual.size());
378 assertTrue(actual.containsAll(dataPoints));
379 assertTrue(dataPoints.containsAll(actual));
380 }
381
382 @Test
383 @Transactional
384 public void getDataPointsByTimeseries_forGivenDuration_returnsThreeOutOfFive() {
385 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("humidity");
386 var start = InstantHelper.now().addDays(-4).toNano();
387 var end = InstantHelper.now().addDays(-2).toNano();
388 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
389 List.of(
390 TimeseriesTestDataGenerator.generateDataPointInteger(InstantHelper.now().addDays(-5).toNano(), 70),
391 TimeseriesTestDataGenerator.generateDataPointInteger(start, 80),
392 TimeseriesTestDataGenerator.generateDataPointInteger(InstantHelper.now().addDays(-3).toNano(), 65),
393 TimeseriesTestDataGenerator.generateDataPointInteger(end, 72),
394 TimeseriesTestDataGenerator.generateDataPointInteger(InstantHelper.now().addDays(-1).toNano(), 88)
395 )
396 );
397
398 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
399 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(start, end, null, null, null);
400 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
401
402 assertEquals(3, actual.size());
403 }
404
405 @Test
406 @Transactional
407 public void getDataPointsByTimeseries_forGivenDuration_returnNone() {
408 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("humidity");
409 var start = InstantHelper.now().addDays(-4).toNano();
410 var end = InstantHelper.now().addDays(-2).toNano();
411 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
412 List.of(
413 TimeseriesTestDataGenerator.generateDataPointInteger(start - 1000, 70),
414 TimeseriesTestDataGenerator.generateDataPointInteger(start - 1100, 80),
415 TimeseriesTestDataGenerator.generateDataPointInteger(start - 1200, 65),
416 TimeseriesTestDataGenerator.generateDataPointInteger(end + 10000, 72),
417 TimeseriesTestDataGenerator.generateDataPointInteger(end + 1000, 88)
418 )
419 );
420
421 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
422 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(start, end, null, null, null);
423 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
424
425 assertEquals(0, actual.size());
426 }
427
428
429
430
431
432 @Test
433 @Transactional
434 public void saveDataPoint_non_unique_returnOverwritten() {
435 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("uniqueness-test-1");
436
437
438 var timeseriesDataPoint1 = new TimeseriesDataPoint(1708067683056880001L, "value 1");
439 var timeseriesDataPoint2 = new TimeseriesDataPoint(1708067683056880002L, "value 2");
440 var timeseriesDataPoint3 = new TimeseriesDataPoint(1708067683056880003L, "value 3");
441
442 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(
443 List.of(timeseriesDataPoint1, timeseriesDataPoint2, timeseriesDataPoint3)
444 );
445
446 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
447
448
449 var timeseriesDataPoint3New = new TimeseriesDataPoint(1708067683056880003L, "value 3 UPDATED");
450 var timeseriesDataPoint4 = new TimeseriesDataPoint(1708067683056880004L, "value 4");
451 var timeseriesDataPoint5 = new TimeseriesDataPoint(1708067683056880005L, "value 5");
452
453 List<TimeseriesDataPoint> dataPointsContainingNonUnique = new ArrayList<>(
454 List.of(timeseriesDataPoint3New, timeseriesDataPoint4, timeseriesDataPoint5)
455 );
456
457 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPointsContainingNonUnique);
458 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
459 1L,
460 1708067683056880099L,
461 null,
462 null,
463 null
464 );
465 var actual = this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
466
467 assertEquals(5, actual.size());
468 assertEquals("value 1", actual.getFirst().getValue());
469 assertEquals("value 2", actual.get(1).getValue());
470 assertEquals("value 3 UPDATED", actual.get(2).getValue());
471 assertEquals("value 4", actual.get(3).getValue());
472 assertEquals("value 5", actual.get(4).getValue());
473 }
474
475 @Test
476 @Transactional
477 public void saveDataPoint_non_unique_batch_returnExceptionOrSilentlyOverwrite() {
478 var timeseries = TimeseriesTestDataGenerator.generateTimeseries("uniqueness-test-2");
479
480
481 var timeseriesDataPoint1 = new TimeseriesDataPoint(1708067683056880001L, "value 1");
482 var timeseriesDataPoint2 = new TimeseriesDataPoint(1708067683056880001L, "value 2");
483 List<TimeseriesDataPoint> dataPoints = new ArrayList<>(List.of(timeseriesDataPoint1, timeseriesDataPoint2));
484
485
486
487 try {
488 this.timeseriesService.saveDataPoints(container.getId(), timeseries, dataPoints);
489 TimeseriesDataPointsQueryParams queryParams = new TimeseriesDataPointsQueryParams(
490 1L,
491 1908067683056880001L,
492 null,
493 null,
494 null
495 );
496 var retrievedTimeseries =
497 this.timeseriesService.getDataPointsByTimeseries(container.getId(), timeseries, queryParams);
498 assertEquals(1, retrievedTimeseries.size());
499 assertEquals("value 2", retrievedTimeseries.getFirst().getValue());
500 } catch (InvalidBodyException ex) {
501 assertTrue(true);
502 } catch (Exception ex) {
503 fail("An unexpected exception was thrown.");
504 }
505 }
506 }