460 lines
21 KiB
Java
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);
|
|
}
|