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

285 lines
14 KiB
Java

package jmri.server.json.power;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Locale;
import javax.servlet.http.HttpServletResponse;
import jmri.InstanceManager;
import jmri.JmriException;
import jmri.PowerManager;
import jmri.jmrix.internal.InternalSystemConnectionMemo;
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 org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
*
* @author Paul Bender
* @author Randall Wood
*/
public class JsonPowerSocketServiceTest {
private final Locale locale = Locale.ENGLISH;
@Test
public void testPowerChange() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode message = connection.getObjectMapper().readTree("{}");
JsonPowerSocketService service = new JsonPowerSocketService(connection);
PowerManager power = InstanceManager.getDefault(PowerManager.class);
assertEquals( 1, power.getPropertyChangeListeners().length, "One listener");
power.setPower(PowerManager.UNKNOWN);
service.onMessage(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
assertEquals( 2, power.getPropertyChangeListeners().length, "Two listeners");
message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals(JSON.UNKNOWN, message.path(JSON.DATA).path(JSON.STATE).asInt());
power.setPower(PowerManager.ON);
message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals(JSON.ON, message.path(JSON.DATA).path(JSON.STATE).asInt());
power.setPower(PowerManager.OFF);
message = connection.getMessage();
assertNotNull( message, "Message is not null");
assertEquals(JSON.OFF, message.path(JSON.DATA).path(JSON.STATE).asInt());
service.onClose();
assertEquals( 1, power.getPropertyChangeListeners().length, "One listener");
}
@Test
public void testOnMessageChange() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode message = connection.getObjectMapper().readTree("{\"state\":2}"); // Power ON
JsonPowerSocketService service = new JsonPowerSocketService(connection);
PowerManager power = InstanceManager.getDefault(PowerManager.class);
power.setPower(PowerManager.UNKNOWN);
service.onMessage(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
assertEquals(PowerManager.ON, power.getPower());
message = connection.getObjectMapper().readTree("{\"name\":\"Internal\", \"state\":4}"); // Power OFF, named connection
service.onMessage(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
assertEquals(PowerManager.OFF, power.getPower());
message = connection.getObjectMapper().readTree("{\"state\":0}"); // JSON Power UNKNOWN
service.onMessage(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.POST, 42));
assertEquals(PowerManager.OFF, power.getPower()); // did not change
JsonNode messageEx = connection.getObjectMapper().readTree("{\"state\":1}"); // JSON Invalid
JsonException ex = assertThrows( JsonException.class, () ->
service.onMessage(JsonPowerServiceFactory.POWER, messageEx,
new JsonRequest(locale, JSON.V5, JSON.POST, 42)),
"Expected exception not thrown");
assertEquals(HttpServletResponse.SC_BAD_REQUEST, ex.getCode());
assertEquals("Attempting to set object type power to unknown state 1.", ex.getMessage());
assertEquals(PowerManager.OFF, power.getPower()); // did not change
service.onClose();
}
@Test
public void testOnList() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode message = connection.getObjectMapper().readTree("{\"state\":0}"); // Power UNKNOWN
JsonPowerSocketService service = new JsonPowerSocketService(connection);
PowerManager power = InstanceManager.getDefault(PowerManager.class);
power.setPower(PowerManager.UNKNOWN);
service.onList(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.GET, 0));
message = connection.getMessage();
assertNotNull(message);
assertTrue(message.isArray());
assertEquals(JsonPowerServiceFactory.POWER, message.path(0).path(JSON.TYPE).asText());
assertEquals(JSON.UNKNOWN, message.path(0).path(JSON.DATA).path(JSON.STATE).asInt(-1));
assertEquals("Internal", message.path(0).path(JSON.DATA).path(JSON.NAME).asText());
assertTrue(message.path(0).path(JSON.DATA).path(JSON.DEFAULT).asBoolean());
power.setPower(PowerManager.ON);
message = connection.getMessage();
assertNotNull(message);
assertTrue(message.isObject());
assertEquals(JsonPowerServiceFactory.POWER, message.path(JSON.TYPE).asText());
assertEquals(JSON.ON, message.path(JSON.DATA).path(JSON.STATE).asInt(-1));
assertEquals("Internal", message.path(JSON.DATA).path(JSON.NAME).asText());
assertTrue(message.path(JSON.DATA).path(JSON.DEFAULT).asBoolean());
service.onClose();
}
@Test
public void testSendingErrors() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
final JsonNode message = connection.getObjectMapper().readTree("{\"state\":0}"); // Power UNKNOWN
TestJsonPowerHttpService http = new TestJsonPowerHttpService(connection.getObjectMapper());
JsonPowerSocketService service = new JsonPowerSocketService(connection, http);
http.setThrowException(true);
JsonException ex = Assertions.assertThrowsExactly(JsonException.class, () ->
service.onMessage(JsonPowerServiceFactory.POWER, message,
new JsonRequest(locale, JSON.V5, JSON.POST, 0)));
assertNotNull(ex);
}
@Test
public void testNoPowerManager() throws IOException, JmriException, JsonException {
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonNode message = connection.getObjectMapper().readTree("{\"state\":0}"); // Power UNKNOWN
JsonPowerSocketService service = new JsonPowerSocketService(connection);
InstanceManager.reset(PowerManager.class);
service.onMessage(JsonPowerServiceFactory.POWER, message, new JsonRequest(locale, JSON.V5, JSON.GET, 42));
message = connection.getMessage();
assertNotNull(message);
assertEquals(JsonPowerServiceFactory.POWER, message.path(JSON.TYPE).asText());
assertEquals(JSON.UNKNOWN, message.path(JSON.DATA).path(JSON.STATE).asInt(-1));
}
/**
* Core regression test for the multi-connection isolation bug: a client
* that subscribed without a prefix (i.e. the JMRI web page power button)
* must NOT receive updates when an unrelated connection's power state
* changes — only updates for the default connection should arrive.
*/
@Test
public void testDefaultClientIgnoresSecondConnection() throws IOException, JmriException, JsonException {
PowerManager defaultPm = InstanceManager.getDefault(PowerManager.class);
defaultPm.setPower(PowerManager.OFF);
InternalSystemConnectionMemo memo2 = new InternalSystemConnectionMemo("J", "Juliet", false);
PowerManager pm2 = memo2.get(PowerManager.class);
assertNotNull(pm2);
pm2.setPower(PowerManager.OFF);
InstanceManager.setDefault(PowerManager.class, defaultPm);
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonPowerSocketService service = new JsonPowerSocketService(connection);
// Client subscribes with no prefix — should track only the default manager
service.onMessage(JsonPowerServiceFactory.POWER, connection.getObjectMapper().createObjectNode(),
new JsonRequest(locale, JSON.V5, JSON.GET, 0));
int messageCount = connection.getMessages().size();
// Change the second connection — client must NOT be notified
pm2.setPower(PowerManager.ON);
assertEquals(messageCount, connection.getMessages().size(),
"Client subscribed to default must not receive updates from second connection");
// Change the default connection — client MUST be notified
defaultPm.setPower(PowerManager.ON);
assertEquals(messageCount + 1, connection.getMessages().size(),
"Client subscribed to default must receive update when default connection changes");
assertEquals(JSON.ON, connection.getMessage().path(JSON.DATA).path(JSON.STATE).asInt());
service.onClose();
}
/**
* A client that subscribed with a specific prefix must only receive updates
* for that connection; changes to the default connection must be ignored.
*/
@Test
public void testPrefixClientIgnoresDefaultConnection() throws IOException, JmriException, JsonException {
PowerManager defaultPm = InstanceManager.getDefault(PowerManager.class);
defaultPm.setPower(PowerManager.OFF);
InternalSystemConnectionMemo memo2 = new InternalSystemConnectionMemo("J", "Juliet", false);
PowerManager pm2 = memo2.get(PowerManager.class);
assertNotNull(pm2);
pm2.setPower(PowerManager.OFF);
InstanceManager.setDefault(PowerManager.class, defaultPm);
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonPowerSocketService service = new JsonPowerSocketService(connection);
ObjectNode data = connection.getObjectMapper().createObjectNode();
data.put(JSON.PREFIX, "J");
service.onMessage(JsonPowerServiceFactory.POWER, data, new JsonRequest(locale, JSON.V5, JSON.GET, 0));
int messageCount = connection.getMessages().size();
// Change default connection — client subscribed to "J" must NOT be notified
defaultPm.setPower(PowerManager.ON);
assertEquals(messageCount, connection.getMessages().size(),
"Client subscribed to prefix J must not receive updates from default connection");
// Change "J" connection — client MUST be notified
pm2.setPower(PowerManager.ON);
assertEquals(messageCount + 1, connection.getMessages().size(),
"Client subscribed to prefix J must receive update when J connection changes");
assertEquals(JSON.ON, connection.getMessage().path(JSON.DATA).path(JSON.STATE).asInt());
service.onClose();
}
/**
* onList must subscribe to all connections; updates from any manager must
* be delivered to the client.
*/
@Test
public void testListSubscribesToAllConnections() throws IOException, JmriException, JsonException {
PowerManager defaultPm = InstanceManager.getDefault(PowerManager.class);
defaultPm.setPower(PowerManager.OFF);
InternalSystemConnectionMemo memo2 = new InternalSystemConnectionMemo("J", "Juliet", false);
PowerManager pm2 = memo2.get(PowerManager.class);
assertNotNull(pm2);
pm2.setPower(PowerManager.OFF);
InstanceManager.setDefault(PowerManager.class, defaultPm);
JsonMockConnection connection = new JsonMockConnection((DataOutputStream) null);
JsonPowerSocketService service = new JsonPowerSocketService(connection);
service.onList(JsonPowerServiceFactory.POWER, connection.getObjectMapper().createObjectNode(),
new JsonRequest(locale, JSON.V5, JSON.GET, 0));
int messageCount = connection.getMessages().size();
// Both connections should trigger notifications after onList
defaultPm.setPower(PowerManager.ON);
assertEquals(messageCount + 1, connection.getMessages().size(),
"onList client must receive update from default connection");
pm2.setPower(PowerManager.ON);
assertEquals(messageCount + 2, connection.getMessages().size(),
"onList client must receive update from second connection");
service.onClose();
}
@BeforeEach
public void setUp() throws Exception {
JUnitUtil.setUp();
JUnitUtil.resetProfileManager();
JUnitUtil.initInternalTurnoutManager();
JUnitUtil.initInternalLightManager();
JUnitUtil.initInternalSensorManager();
JUnitUtil.initDebugThrottleManager();
JUnitUtil.initDebugPowerManager();
}
@AfterEach
public void tearDown() throws Exception {
JUnitUtil.tearDown();
}
private static class TestJsonPowerHttpService extends JsonPowerHttpService {
private boolean throwException = false;
TestJsonPowerHttpService(ObjectMapper mapper) {
super(mapper);
}
@Override
public JsonNode doPost(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
if (throwException) {
throwException = false;
throw new JsonException(499, "Mock Exception", request.id);
}
return super.doPost(type, name, data, request);
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
}
}