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