Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
*/
public enum ResultType {
SECURITY_ANALYSIS,
STATE_ESTIMATION
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.services;

import org.gridsuite.monitor.commons.ResultType;
import org.springframework.stereotype.Service;

import java.util.UUID;

/**
* @author Achour BERRAHMA <achour.berrahma at rte-france.com>
*/
@Service
public class StateEstimationResultProvider implements ResultProvider {
private final StateEstimationService stateEstimationService;

public StateEstimationResultProvider(StateEstimationService stateEstimationService) {
this.stateEstimationService = stateEstimationService;
}

@Override
public ResultType getType() {
return ResultType.STATE_ESTIMATION;
}

@Override
public String getResult(UUID resultId) {
return stateEstimationService.getResult(resultId);
}

@Override
public void deleteResult(UUID resultId) {
stateEstimationService.deleteResult(resultId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.services;

import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.List;
import java.util.UUID;

/**
* @author Achour BERRAHMA <achour.berrahma at rte-france.com>
*/
@Service
public class StateEstimationService {
private static final Logger LOGGER = LoggerFactory.getLogger(StateEstimationService.class);
static final String SE_API_VERSION = "v1";
private static final String DELIMITER = "/";

private final RestTemplate restTemplate;

@Setter
private String stateEstimationServerBaseUri;

private String getStateEstimationServerBaseUri() {
return this.stateEstimationServerBaseUri + DELIMITER + SE_API_VERSION + DELIMITER;
}
Comment on lines +36 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize the base URI to avoid //v1//results requests.

With a trailing slash in the default base URI and leading slashes in paths, the constructed URLs contain double slashes, which can break strict path matching and won’t match the test expectations.

🔧 Proposed fix
-    private String getStateEstimationServerBaseUri() {
-        return this.stateEstimationServerBaseUri + DELIMITER + SE_API_VERSION + DELIMITER;
-    }
+    private String getStateEstimationServerBaseUri() {
+        String baseUri = this.stateEstimationServerBaseUri;
+        if (baseUri.endsWith(DELIMITER)) {
+            baseUri = baseUri.substring(0, baseUri.length() - 1);
+        }
+        return baseUri + DELIMITER + SE_API_VERSION;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@monitor-server/src/main/java/org/gridsuite/monitor/server/services/StateEstimationService.java`
around lines 36 - 38, The helper getStateEstimationServerBaseUri() currently
concatenates stateEstimationServerBaseUri, DELIMITER and SE_API_VERSION which
can produce double slashes when stateEstimationServerBaseUri already ends with a
slash; change it to normalize the base by trimming any trailing '/' from
stateEstimationServerBaseUri (or conditionally avoid adding DELIMITER when it
already ends with one) before appending DELIMITER + SE_API_VERSION + DELIMITER
so constructed URLs like //v1//results are eliminated; update any callers
relying on getStateEstimationServerBaseUri() accordingly.


public StateEstimationService(
RestTemplateBuilder restTemplateBuilder,
@Value("${gridsuite.services.state-estimation-server.base-uri:http://state-estimation-server/}") String stateEstimationServerBaseUri) {
this.stateEstimationServerBaseUri = stateEstimationServerBaseUri;
this.restTemplate = restTemplateBuilder.build();
}

public String getResult(UUID resultUuid) {
LOGGER.info("Fetching state estimation result {}", resultUuid);

var path = UriComponentsBuilder.fromPath("/results/{resultUuid}")
.buildAndExpand(resultUuid)
.toUriString();

return restTemplate.exchange(getStateEstimationServerBaseUri() + path, HttpMethod.GET, null, String.class).getBody();
}

public void deleteResult(UUID resultUuid) {
LOGGER.info("Deleting state estimation result {}", resultUuid);

var path = UriComponentsBuilder.fromPath("/results")
.queryParam("resultsUuids", List.of(resultUuid))
.build()
.toUriString();

restTemplate.delete(getStateEstimationServerBaseUri() + path);
}
}
4 changes: 3 additions & 1 deletion monitor-server/src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ gridsuite:
report-server:
base-uri: http://localhost:5028
security-analysis-server:
base-uri: http://localhost:5023
base-uri: http://localhost:5023
state-estimation-server:
base-uri: http://localhost:6040
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.services;

import org.gridsuite.monitor.commons.ResultType;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

/**
* @author Achour BERRAHMA <achour.berrahma at rte-france.com>
*/
class StateEstimationResultProviderTest {
private final StateEstimationService stateEstimationService =
Mockito.mock(StateEstimationService.class);

private final StateEstimationResultProvider provider =
new StateEstimationResultProvider(stateEstimationService);

@Test
void getTypeShouldReturnStateEstimation() {
assertThat(provider.getType())
.isEqualTo(ResultType.STATE_ESTIMATION);
}

@Test
void getResultShouldDelegateToStateEstimationService() {
UUID id = UUID.randomUUID();
String expected = "result";

when(stateEstimationService.getResult(id)).thenReturn(expected);

String result = provider.getResult(id);

assertThat(result).isEqualTo(expected);
verify(stateEstimationService).getResult(id);
verifyNoMoreInteractions(stateEstimationService);
}

@Test
void deleteResultShouldDelegateToStateEstimationService() {
UUID id = UUID.randomUUID();

doNothing().when(stateEstimationService).deleteResult(id);

provider.deleteResult(id);

verify(stateEstimationService).deleteResult(id);
verifyNoMoreInteractions(stateEstimationService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.services;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;
import org.springframework.test.web.client.response.MockRestResponseCreators;
import org.springframework.web.client.RestClientException;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

/**
* @author Achour BERRAHMA <achour.berrahma at rte-france.com>
*/
@RestClientTest(StateEstimationService.class)
@ContextConfiguration(classes = { StateEstimationService.class })
class StateEstimationServiceTest {

private static final UUID RESULT_UUID = UUID.randomUUID();
private static final String RESULT_BODY = "{\"status\":\"OK\"}";

@Autowired
private StateEstimationService stateEstimationService;

@Autowired
private MockRestServiceServer server;

@AfterEach
void tearDown() {
server.verify();
}

@Test
void getResult() {
server.expect(MockRestRequestMatchers.method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.requestTo(
"http://state-estimation-server/v1/results/" + RESULT_UUID
))
.andRespond(MockRestResponseCreators.withSuccess(
RESULT_BODY,
MediaType.APPLICATION_JSON
));

String result = stateEstimationService.getResult(RESULT_UUID);

assertThat(result).isEqualTo(RESULT_BODY);
}

@Test
void getResultFailed() {
server.expect(MockRestRequestMatchers.method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.requestTo(
"http://state-estimation-server/v1/results/" + RESULT_UUID
))
.andRespond(MockRestResponseCreators.withServerError());

assertThatThrownBy(() -> stateEstimationService.getResult(RESULT_UUID))
.isInstanceOf(RestClientException.class);
}

@Test
void deleteResult() {
server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
.andExpect(MockRestRequestMatchers.requestTo(
"http://state-estimation-server/v1/results?resultsUuids=" + RESULT_UUID
))
.andRespond(MockRestResponseCreators.withSuccess());

assertThatNoException().isThrownBy(() -> stateEstimationService.deleteResult(RESULT_UUID));
}

@Test
void deleteResultFailed() {
server.expect(MockRestRequestMatchers.method(HttpMethod.DELETE))
.andExpect(MockRestRequestMatchers.requestTo(
"http://state-estimation-server/v1/results?resultsUuids=" + RESULT_UUID
))
.andRespond(MockRestResponseCreators.withServerError());

assertThatThrownBy(() -> stateEstimationService.deleteResult(RESULT_UUID))
.isInstanceOf(RestClientException.class);
}
}