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