Files
JIMRI/java/test/jmri/server/json/util/JsonUtilSocketServiceTest.java
2026-06-17 14:00:51 +02:00

460 lines
21 KiB
Java

package jmri.server.json.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.*;
import java.util.Locale;
import jmri.InstanceManager;
import jmri.JmriException;
import jmri.server.json.JsonServerPreferences;
import jmri.jmrit.display.Editor;
import jmri.jmrit.display.controlPanelEditor.ControlPanelEditor;
import jmri.jmrit.display.layoutEditor.LayoutEditor;
import jmri.jmrit.display.panelEditor.PanelEditor;
import jmri.jmrit.display.switchboardEditor.SwitchboardEditor;
import jmri.profile.NullProfile;
import jmri.server.json.JSON;
import jmri.server.json.JsonException;
import jmri.server.json.JsonMockConnection;
import jmri.server.json.JsonRequest;
import jmri.util.JUnitUtil;
import jmri.util.junit.annotations.DisabledIfHeadless;
import jmri.web.server.WebServerPreferences;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Randall Wood
*/
public class JsonUtilSocketServiceTest {
private final Locale locale = Locale.ENGLISH;
@BeforeEach
public void setUp(@TempDir File folder) throws IOException {
JUnitUtil.setUp();
// list open windows when running tests
JUnitUtil.resetWindows(true, false);
JUnitUtil.resetNodeIdentity();
JUnitUtil.resetProfileManager(
new NullProfile("JsonUtilHttpServiceTest", "12345678", folder));
JUnitUtil.initConfigureManager();
// Initialize mock PermissionManager for session authentication tests
InstanceManager.store(new MockPermissionManager(), jmri.PermissionManager.class);
}
@AfterEach
public void tearDown() {
JUnitUtil.resetWindows(false, false);
JUnitUtil.deregisterBlockManagerShutdownTask();
JUnitUtil.tearDown();
}
/**
* Test of onMessage method, of class JsonUtilSocketService. Tests only
* responses that are expected to be consistent between a
*
* @throws java.io.IOException if an unexpected exception occurs.
* @throws jmri.JmriException if an unexpected exception occurs.
* @throws jmri.server.json.JsonException if an unexpected exception occurs.
*/
@Test
public void testOnMessage() throws IOException, JmriException, JsonException {
JsonNode message;
InstanceManager.getDefault(JsonServerPreferences.class).setValidateServerMessages(true);
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
// JSON.LOCALE
instance.onMessage(JSON.LOCALE, empty, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
assertNull(connection.getMessage()); // assert no reply
// JSON.PING
instance.onMessage(JSON.PING, empty, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
message = connection.getMessage();
assertNotNull( message, "message is not null");
JsonNode result = message.path(JSON.TYPE);
assertNotNull(result);
assertTrue(JsonNode.class.isInstance(result));
assertEquals(JSON.PONG, result.asText());
assertTrue(message.path(JSON.DATA).isMissingNode());
// JSON.RAILROAD
WebServerPreferences wsp = InstanceManager.getDefault(WebServerPreferences.class);
instance.onMessage(JSON.RAILROAD, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
message = connection.getMessage();
assertNotNull( message, "message is not null");
result = message.path(JSON.DATA);
assertNotNull(result);
assertEquals(JSON.RAILROAD, message.path(JSON.TYPE).asText());
assertEquals( wsp.getRailroadName(), result.path(JSON.NAME).asText(), "Railroad name matches");
wsp.setRailroadName("test railroad");
message = connection.getMessage();
assertNotNull( message, "message is not null");
result = message.path(JSON.DATA);
assertNotNull(result);
assertEquals(JSON.RAILROAD, message.path(JSON.TYPE).asText());
assertEquals( wsp.getRailroadName(), result.path(JSON.NAME).asText(), "Railroad name matches");
// JSON.NETWORK_SERVICE (should return 404 because not running the
// requested service)
message = connection.getObjectMapper().createObjectNode().put(JSON.NAME, JSON.ZEROCONF_SERVICE_TYPE);
final var finalMessage = message;
JsonException ex = assertThrows(JsonException.class,
() -> instance.onMessage(JSON.NETWORK_SERVICE, finalMessage,
new JsonRequest(locale, JSON.V5, JSON.GET, 42)));
assertEquals( 404, ex.getCode(), "HTTP Not Found");
assertEquals( "Unable to access networkService _jmri-json._tcp.local..", ex.getMessage(),
"Error Message");
// JSON.GOODBYE
instance.onMessage(JSON.GOODBYE, empty, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
message = connection.getMessage();
assertNotNull( message, "message is not null");
result = message.path(JSON.TYPE);
assertNotNull(result);
assertTrue(JsonNode.class.isInstance(result));
assertEquals(JSON.GOODBYE, result.asText());
assertTrue(message.path(JSON.DATA).isMissingNode());
}
/**
* Test of onMessage method, of class JsonUtilSocketService.
* Tests PANEL JSON type if not running headless.
*
* @throws java.io.IOException if an unexpected exception occurs.
* @throws jmri.JmriException if an unexpected exception occurs.
* @throws jmri.server.json.JsonException if an unexpected exception occurs.
*/
@Test
@DisabledIfHeadless
public void testOnMessagePanels() throws IOException, JmriException, JsonException {
Editor editor = new SwitchboardEditor("json test switchboard");
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
JsonException ex = assertThrows(JsonException.class,
() -> instance.onMessage(JSON.PANELS, empty,
new JsonRequest(locale, JSON.V5, JSON.GET, 42)));
assertEquals( 404, ex.getCode(), "HTTP Not Found");
assertEquals( "Unable to access panel .", ex.getMessage(), "Error Message");
JsonNode data =
connection.getObjectMapper().createObjectNode().put(JSON.NAME, "Switchboard/json%20test%20switchboard");
instance.onMessage(JSON.PANEL, data, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
JUnitUtil.dispose(editor.getTargetFrame());
JUnitUtil.dispose(editor);
}
/**
* Test of onList method, of class JsonUtilSocketService. Does not test
* CONFIG_PROFILE JSON type, see {@link #testOnListConfigProfile()} for
* that. Does not test PANEL JSON type, see {@link #testOnListPanels()} for
* that.
*
* @throws java.io.IOException if an unexpected exception occurs.
* @throws jmri.JmriException if an unexpected exception occurs.
* @throws jmri.server.json.JsonException if an unexpected exception occurs.
*/
@Test
public void testOnList() throws IOException, JmriException, JsonException {
ObjectMapper mapper = new ObjectMapper();
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
JsonUtilHttpService helper = new JsonUtilHttpService(mapper);
InstanceManager.getDefault(JsonServerPreferences.class).setHeartbeatInterval(10);
instance.onList(JSON.METADATA, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
assertEquals(helper.getMetadata(new JsonRequest(locale, JSON.V5, JSON.GET, 42)), connection.getMessage());
instance.onList(JSON.NETWORK_SERVICES, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
assertEquals(helper.getNetworkServices(new JsonRequest(locale, JSON.V5, JSON.GET, 42)), connection.getMessage());
instance.onList(JSON.SYSTEM_CONNECTIONS, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
assertEquals(helper.getSystemConnections(new JsonRequest(locale, JSON.V5, JSON.GET, 42)), connection.getMessage());
}
/**
* Test of onList method for CONFIG_PROFILE JSON type, of class
* JsonUtilSocketService.
*
* @throws java.io.IOException if an unexpected exception occurs.
* @throws jmri.JmriException if an unexpected exception occurs.
* @throws jmri.server.json.JsonException if an unexpected exception occurs.
*/
@Test
public void testOnListConfigProfile() throws IOException, JmriException, JsonException {
ObjectMapper mapper = new ObjectMapper();
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
JsonUtilHttpService helper = new JsonUtilHttpService(mapper);
InstanceManager.getDefault(JsonServerPreferences.class).setHeartbeatInterval(10);
instance.onList(JSON.CONFIG_PROFILES, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
assertEquals(helper.getConfigProfiles(new JsonRequest(locale, JSON.V5, JSON.GET, 42)), connection.getMessage());
}
/**
* Test of onList method, of class JsonUtilSocketService. Tests PANEL JSON
* type if not running headless.
*
* @throws java.io.IOException if an unexpected exception occurs.
* @throws jmri.JmriException if an unexpected exception occurs.
* @throws jmri.server.json.JsonException if an unexpected exception occurs.
*/
@Test
@DisabledIfHeadless
public void testOnListPanels() throws IOException, JmriException, JsonException {
Editor switchboard = new SwitchboardEditor("json test switchboard");
Editor controlPanel = new ControlPanelEditor("json test control panel");
Editor layoutPanel = new LayoutEditor("json test layout panel");
Editor panel = new PanelEditor("json test panel");
Editor disabled = new PanelEditor("disabled json test panel");
disabled.setAllowInFrameServlet(false);
// 5 editors should return array of 4 since one is barred
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
instance.onList(JSON.PANELS, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
JsonNode message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertTrue( message.isArray(), "Message is array");
assertEquals( 4, message.size(),
() -> "Array has four elements, what panel was left in place that triggered this? "
+ message.toString());
JUnitUtil.dispose(switchboard.getTargetFrame());
JUnitUtil.dispose(switchboard);
JUnitUtil.dispose(controlPanel.getTargetFrame());
JUnitUtil.dispose(controlPanel);
JUnitUtil.dispose(layoutPanel.getTargetFrame());
JUnitUtil.dispose(layoutPanel);
JUnitUtil.dispose(panel.getTargetFrame());
JUnitUtil.dispose(panel);
}
@Test
public void testRRNameListener() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode empty = connection.getObjectMapper().createObjectNode();
TestJsonUtilHttpService httpService = new TestJsonUtilHttpService(connection.getObjectMapper());
JsonUtilSocketService instance = new JsonUtilSocketService(connection, httpService);
WebServerPreferences preferences = InstanceManager.getDefault(WebServerPreferences.class);
assertEquals( 0, preferences.getPropertyChangeListeners().length, "No preferences listener");
instance.onMessage(JSON.RAILROAD, empty, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
JsonNode message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals( preferences.getRailroadName(),
message.path(JSON.DATA).path(JSON.NAME).asText(),
"Message has RR Name");
assertEquals( 1, preferences.getPropertyChangeListeners().length, "There is a preferences listener");
preferences.setRailroadName("New Name");
message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals( preferences.getRailroadName(),
message.path(JSON.DATA).path(JSON.NAME).asText(),
"Message has RR Name");
// force JsonException
httpService.setThrowException(true);
preferences.setRailroadName("Another New Name");
message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals( JsonException.ERROR, message.path(JSON.TYPE).asText(), "Message is error");
assertEquals( 499, message.path(JSON.DATA).path(JsonException.CODE).asInt(),
"Error code is 499");
// force IOException
assertEquals( 1, preferences.getPropertyChangeListeners().length, "There is a preferences listener");
connection.setThrowIOException(true);
preferences.setRailroadName("Yet Another New Name");
assertEquals( 0, preferences.getPropertyChangeListeners().length,
"There is no longer a preferences listener");
}
/**
* Test of onClose method, of class JsonUtilSocketService. This tests that
* listeners are removed after a message triggering the addition of a
* listener is sent.
*
* @throws JsonException if an exception unexpected in the context of these
* tests occurs
* @throws JmriException if an exception unexpected in the context of these
* tests occurs
* @throws IOException if an exception unexpected in the context of these
* tests occurs
*/
@Test
public void testOnClose() throws IOException, JmriException, JsonException {
WebServerPreferences wsp = InstanceManager.getDefault(WebServerPreferences.class);
assertEquals( 0, wsp.getPropertyChangeListeners().length, "listeners");
JsonUtilSocketService instance = new JsonUtilSocketService(new JsonMockConnection((DataOutputStream) null));
instance.onMessage(JSON.RAILROAD, instance.getConnection().getObjectMapper().nullNode(),
new JsonRequest(locale, JSON.V5, JSON.GET, 0));
assertEquals( 1, wsp.getPropertyChangeListeners().length, "listeners");
instance.onClose();
assertEquals( 0, wsp.getPropertyChangeListeners().length, "listeners");
}
/**
* Test SESSION_LOGIN message handling via onMessage.
*
* @throws IOException if test fails unexpectedly
* @throws JmriException if test fails unexpectedly
* @throws JsonException expected for invalid credentials in test
*/
@Test
public void testSessionLoginMessage() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
ObjectNode loginData = connection.getObjectMapper().createObjectNode();
loginData.put("username", "testuser");
loginData.put("password", "testpass");
try {
instance.onMessage(JSON.SESSION_LOGIN, loginData,
new JsonRequest(locale, JSON.V5, JSON.POST, 42));
JsonNode message = connection.getMessage();
assertNotNull(message, "Should receive a message");
// In test environment without configured users, should get error
} catch (JsonException ex) {
// Expected - no valid users configured in test
assertTrue(ex.getCode() == 401 || ex.getCode() == 403,
"Expected unauthorized or forbidden error");
}
}
/**
* Test SESSION_LOGOUT message handling via onMessage.
*
* @throws IOException if test fails unexpectedly
* @throws JmriException if test fails unexpectedly
* @throws JsonException if test fails unexpectedly
*/
@Test
public void testSessionLogoutMessage() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonUtilSocketService instance = new JsonUtilSocketService(connection);
ObjectNode logoutData = connection.getObjectMapper().createObjectNode();
logoutData.put("token", "test-session-token");
logoutData.put(JSON.USERNAME, "testuser");
instance.onMessage(JSON.SESSION_LOGOUT, logoutData,
new JsonRequest(locale, JSON.V5, JSON.POST, 42));
JsonNode message = connection.getMessage();
assertNotNull(message, "Should receive logout confirmation");
assertEquals(JSON.SESSION_LOGOUT, message.path(JSON.TYPE).asText(),
"Message type should be sessionLogout");
assertEquals("test-session-token", message.path(JSON.DATA).path("authenticationToken").asText(),
"Should return the token");
}
/**
* Mock PermissionManager for testing session authentication.
*/
private static class MockPermissionManager implements jmri.PermissionManager {
@Override
public boolean remoteLogin(StringBuilder sessionId, java.util.Locale locale,
String username, String password) {
// Reject guest users
if ("guest".equals(username)) {
return false;
}
// Accept any non-guest user for testing
sessionId.append("test-session-").append(username);
return true;
}
@Override
public void remoteLogout(String sessionId) {
// No-op for testing
}
@Override
public boolean isAGuestUser(String username) {
return "guest".equals(username);
}
@Override
public boolean isAGuestUser(jmri.User user) {
return user != null && isAGuestUser(user.getUserName());
}
// Stub implementations for other required methods
@Override
public jmri.Role addRole(String name) { return null; }
@Override
public void removeRole(String name) {}
@Override
public jmri.User addUser(String username, String password) { return null; }
@Override
public void removeUser(String username) {}
@Override
public void changePassword(String newPassword, String oldPassword) {}
@Override
public boolean login(String username, String password) { return false; }
@Override
public void logout() {}
@Override
public String getCurrentUserName() { return null; }
@Override
public boolean isCurrentUser(String username) { return false; }
@Override
public boolean isCurrentUser(jmri.User user) { return false; }
@Override
public boolean isCurrentUserPermittedToChangePassword() { return false; }
@Override
public boolean isLoggedIn() { return false; }
@Override
public boolean isRemotelyLoggedIn(String sessionId) { return false; }
@Override
public void addLoginListener(LoginListener listener) {}
@Override
public boolean isEnabled() { return true; }
@Override
public void setEnabled(boolean enabled) {}
@Override
public boolean isAllowEmptyPasswords() { return false; }
@Override
public void setAllowEmptyPasswords(boolean value) {}
@Override
public boolean hasAtLeastPermission(jmri.Permission permission, jmri.PermissionValue minValue) { return true; }
@Override
public boolean hasAtLeastRemotePermission(String sessionId, jmri.Permission permission, jmri.PermissionValue minValue) { return true; }
@Override
public boolean ensureAtLeastPermission(jmri.Permission permission, jmri.PermissionValue minValue) { return true; }
@Override
public void registerOwner(jmri.PermissionOwner owner) {}
@Override
public void registerPermission(jmri.Permission permission) {}
@Override
public void storePermissionSettings() {}
}
private static class TestJsonUtilHttpService extends JsonUtilHttpService {
private boolean throwException = false;
TestJsonUtilHttpService(ObjectMapper mapper) {
super(mapper);
}
@Override
public JsonNode doGet(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
if (throwException) {
throwException = false;
throw new JsonException(499, "Mock Exception", request.id);
}
return super.doGet(type, name, data, request);
}
void setThrowException(boolean throwException) {
this.throwException = throwException;
}
}
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JsonUtilSocketServiceTest.class);
}