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); }