proxy progress
This commit is contained in:
parent
b7c994daaf
commit
e696dc6890
|
@ -6,6 +6,8 @@ import com.rusefi.config.generated.Fields;
|
||||||
import com.rusefi.io.commands.HelloCommand;
|
import com.rusefi.io.commands.HelloCommand;
|
||||||
import com.rusefi.io.tcp.BinaryProtocolServer;
|
import com.rusefi.io.tcp.BinaryProtocolServer;
|
||||||
import com.rusefi.io.tcp.TcpIoStream;
|
import com.rusefi.io.tcp.TcpIoStream;
|
||||||
|
import com.rusefi.server.ControllerInfo;
|
||||||
|
import com.rusefi.server.SessionDetails;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
@ -15,13 +17,14 @@ import static com.rusefi.io.tcp.BinaryProtocolServer.getPacketLength;
|
||||||
import static com.rusefi.io.tcp.BinaryProtocolServer.readPromisedBytes;
|
import static com.rusefi.io.tcp.BinaryProtocolServer.readPromisedBytes;
|
||||||
|
|
||||||
public class MockRusEfiDevice {
|
public class MockRusEfiDevice {
|
||||||
private final String authToken;
|
private SessionDetails sessionDetails;
|
||||||
private final String signature;
|
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
|
||||||
public MockRusEfiDevice(String authToken, String signature, Logger logger) {
|
public MockRusEfiDevice(String authToken, String signature, Logger logger) {
|
||||||
this.authToken = authToken;
|
ControllerInfo ci = new ControllerInfo("vehicle", "make", "code", signature);
|
||||||
this.signature = signature;
|
|
||||||
|
sessionDetails = new SessionDetails(ci, authToken, SessionDetails.createOneTimeCode());
|
||||||
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,9 +43,8 @@ public class MockRusEfiDevice {
|
||||||
|
|
||||||
byte command = payload[0];
|
byte command = payload[0];
|
||||||
|
|
||||||
|
|
||||||
if (command == Fields.TS_HELLO_COMMAND) {
|
if (command == Fields.TS_HELLO_COMMAND) {
|
||||||
new HelloCommand(logger, authToken + signature).handle(packet, stream);
|
new HelloCommand(logger, sessionDetails.toJson()).handle(packet, stream);
|
||||||
} else {
|
} else {
|
||||||
handleCommand();
|
handleCommand();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static com.rusefi.binaryprotocol.BinaryProtocol.sleep;
|
|
||||||
import static com.rusefi.server.Backend.LIST_PATH;
|
import static com.rusefi.server.Backend.LIST_PATH;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
@ -49,9 +48,16 @@ public class ServerTest {
|
||||||
Function<String, UserDetails> userDetailsResolver = authToken -> new UserDetails(authToken.substring(0, 5), authToken.charAt(6));
|
Function<String, UserDetails> userDetailsResolver = authToken -> new UserDetails(authToken.substring(0, 5), authToken.charAt(6));
|
||||||
|
|
||||||
CountDownLatch allClientsDisconnected = new CountDownLatch(1);
|
CountDownLatch allClientsDisconnected = new CountDownLatch(1);
|
||||||
|
CountDownLatch onConnected = new CountDownLatch(2);
|
||||||
|
|
||||||
int httpPort = 8000;
|
int httpPort = 8000;
|
||||||
Backend backend = new Backend(userDetailsResolver, httpPort) {
|
Backend backend = new Backend(userDetailsResolver, httpPort) {
|
||||||
|
@Override
|
||||||
|
public void register(ClientConnectionState clientConnectionState) {
|
||||||
|
super.register(clientConnectionState);
|
||||||
|
onConnected.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close(ClientConnectionState inactiveClient) {
|
public void close(ClientConnectionState inactiveClient) {
|
||||||
super.close(inactiveClient);
|
super.close(inactiveClient);
|
||||||
|
@ -68,7 +74,7 @@ public class ServerTest {
|
||||||
public void run() {
|
public void run() {
|
||||||
ClientConnectionState clientConnectionState = new ClientConnectionState(clientSocket, logger, backend.getUserDetailsResolver());
|
ClientConnectionState clientConnectionState = new ClientConnectionState(clientSocket, logger, backend.getUserDetailsResolver());
|
||||||
try {
|
try {
|
||||||
clientConnectionState.sayHello();
|
clientConnectionState.requestControllerInfo();
|
||||||
|
|
||||||
backend.register(clientConnectionState);
|
backend.register(clientConnectionState);
|
||||||
clientConnectionState.runEndlessLoop();
|
clientConnectionState.runEndlessLoop();
|
||||||
|
@ -87,9 +93,7 @@ public class ServerTest {
|
||||||
new MockRusEfiDevice("00000000-1234-1234-1234-123456789012", "rusEFI 2020.07.06.frankenso_na6.2468827536", logger).connect(serverPort);
|
new MockRusEfiDevice("00000000-1234-1234-1234-123456789012", "rusEFI 2020.07.06.frankenso_na6.2468827536", logger).connect(serverPort);
|
||||||
new MockRusEfiDevice("12345678-1234-1234-1234-123456789012", "rusEFI 2020.07.11.proteus_f4.1986715563", logger).connect(serverPort);
|
new MockRusEfiDevice("12345678-1234-1234-1234-123456789012", "rusEFI 2020.07.11.proteus_f4.1986715563", logger).connect(serverPort);
|
||||||
|
|
||||||
|
assertTrue(onConnected.await(30, TimeUnit.SECONDS));
|
||||||
// todo: technically we should have callbacks for 'connect', will make this better if this would be failing
|
|
||||||
sleep(Timeouts.SECOND);
|
|
||||||
|
|
||||||
List<ClientConnectionState> clients = backend.getClients();
|
List<ClientConnectionState> clients = backend.getClients();
|
||||||
assertEquals(2, clients.size());
|
assertEquals(2, clients.size());
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
<orderEntry type="library" scope="TEST" name="javax.json" level="project" />
|
<orderEntry type="library" scope="TEST" name="javax.json" level="project" />
|
||||||
<orderEntry type="library" name="javax.json" level="project" />
|
<orderEntry type="library" name="javax.json" level="project" />
|
||||||
<orderEntry type="library" name="json-simple" level="project" />
|
<orderEntry type="library" name="json-simple" level="project" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="junit" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -19,8 +19,9 @@ import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class Backend {
|
public class Backend {
|
||||||
|
|
||||||
public static final String LIST_PATH = "/list_online";
|
public static final String LIST_PATH = "/list_online";
|
||||||
|
public static final String VERSION_PATH = "/version";
|
||||||
|
public static final String BACKEND_VERSION = "0.0001";
|
||||||
|
|
||||||
private final FkRegex showOnlineUsers = new FkRegex(LIST_PATH,
|
private final FkRegex showOnlineUsers = new FkRegex(LIST_PATH,
|
||||||
(Take) req -> getUsersOnline()
|
(Take) req -> getUsersOnline()
|
||||||
|
@ -33,9 +34,9 @@ public class Backend {
|
||||||
for (ClientConnectionState client : clients) {
|
for (ClientConnectionState client : clients) {
|
||||||
|
|
||||||
JsonObject clientObject = Json.createObjectBuilder()
|
JsonObject clientObject = Json.createObjectBuilder()
|
||||||
.add(UserDetails.USER_ID, client.getUserDetails().getId())
|
.add(UserDetails.USER_ID, client.getUserDetails().getUserId())
|
||||||
.add(UserDetails.USERNAME, client.getUserDetails().getUserName())
|
.add(UserDetails.USERNAME, client.getUserDetails().getUserName())
|
||||||
.add("signature", client.getSignature())
|
.add(ControllerInfo.SIGNATURE, client.getSessionDetails().getControllerInfo().getSignature())
|
||||||
.build();
|
.build();
|
||||||
builder.add(clientObject);
|
builder.add(clientObject);
|
||||||
}
|
}
|
||||||
|
@ -58,6 +59,7 @@ public class Backend {
|
||||||
try {
|
try {
|
||||||
new FtBasic(
|
new FtBasic(
|
||||||
new TkFork(showOnlineUsers,
|
new TkFork(showOnlineUsers,
|
||||||
|
new FkRegex(VERSION_PATH, BACKEND_VERSION),
|
||||||
new FkRegex("/", "<a href='https://rusefi.com/online/'>rusEFI Online</a>")
|
new FkRegex("/", "<a href='https://rusefi.com/online/'>rusEFI Online</a>")
|
||||||
), httpPort
|
), httpPort
|
||||||
).start(Exit.NEVER);
|
).start(Exit.NEVER);
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.rusefi.io.commands.HelloCommand;
|
||||||
import com.rusefi.io.tcp.TcpIoStream;
|
import com.rusefi.io.tcp.TcpIoStream;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -22,12 +21,17 @@ public class ClientConnectionState {
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private final Function<String, UserDetails> userDetailsResolver;
|
private final Function<String, UserDetails> userDetailsResolver;
|
||||||
|
|
||||||
private long lastActivityTimestamp;
|
|
||||||
private boolean isClosed;
|
private boolean isClosed;
|
||||||
private IoStream stream;
|
private IoStream stream;
|
||||||
private IncomingDataBuffer incomingData;
|
private IncomingDataBuffer incomingData;
|
||||||
|
/**
|
||||||
|
* Data from controller
|
||||||
|
*/
|
||||||
|
private SessionDetails sessionDetails;
|
||||||
|
/**
|
||||||
|
* user info from rusEFI database based on auth token
|
||||||
|
*/
|
||||||
private UserDetails userDetails;
|
private UserDetails userDetails;
|
||||||
private String signature;
|
|
||||||
|
|
||||||
public ClientConnectionState(Socket clientSocket, Logger logger, Function<String, UserDetails> userDetailsResolver) {
|
public ClientConnectionState(Socket clientSocket, Logger logger, Function<String, UserDetails> userDetailsResolver) {
|
||||||
this.clientSocket = clientSocket;
|
this.clientSocket = clientSocket;
|
||||||
|
@ -55,19 +59,18 @@ public class ClientConnectionState {
|
||||||
close(clientSocket);
|
close(clientSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sayHello() throws IOException {
|
public void requestControllerInfo() throws IOException {
|
||||||
HelloCommand.send(stream, logger);
|
HelloCommand.send(stream, logger);
|
||||||
byte[] response = incomingData.getPacket(logger, "", false);
|
byte[] response = incomingData.getPacket(logger, "", false);
|
||||||
if (!checkResponseCode(response, BinaryProtocolCommands.RESPONSE_OK))
|
if (!checkResponseCode(response, BinaryProtocolCommands.RESPONSE_OK))
|
||||||
return;
|
return;
|
||||||
String tokenAndSignature = new String(response, 1, response.length - 1);
|
String jsonString = new String(response, 1, response.length - 1);
|
||||||
String authToken = tokenAndSignature.length() > AutoTokenUtil.TOKEN_LENGTH ? tokenAndSignature.substring(0, AutoTokenUtil.TOKEN_LENGTH) : null;
|
sessionDetails = SessionDetails.valueOf(jsonString);
|
||||||
if (!AutoTokenUtil.isToken(authToken))
|
if (!AutoTokenUtil.isToken(sessionDetails.getAuthToken()))
|
||||||
throw new IOException("Invalid token");
|
throw new IOException("Invalid token in " + jsonString);
|
||||||
signature = tokenAndSignature.substring(AutoTokenUtil.TOKEN_LENGTH);
|
|
||||||
|
|
||||||
logger.info(authToken + " New client: " + signature);
|
logger.info(sessionDetails.getAuthToken() + " New client: " + sessionDetails.getControllerInfo());
|
||||||
userDetails = userDetailsResolver.apply(authToken);
|
userDetails = userDetailsResolver.apply(sessionDetails.getAuthToken());
|
||||||
logger.info("User " + userDetails);
|
logger.info("User " + userDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,8 +78,8 @@ public class ClientConnectionState {
|
||||||
return userDetails;
|
return userDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSignature() {
|
public SessionDetails getSessionDetails() {
|
||||||
return signature;
|
return sessionDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void close(Closeable closeable) {
|
private static void close(Closeable closeable) {
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package com.rusefi.server;
|
||||||
|
|
||||||
|
import javax.json.Json;
|
||||||
|
import javax.json.JsonObject;
|
||||||
|
import javax.json.JsonReader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller description without any sensitive information
|
||||||
|
*/
|
||||||
|
public class ControllerInfo {
|
||||||
|
public static final String VEHICLE_NAME = "vehicleName";
|
||||||
|
public static final String ENGINE_MAKE = "engineMake";
|
||||||
|
public static final String ENGINE_CODE = "engineCode";
|
||||||
|
public static final String SIGNATURE = "signature";
|
||||||
|
|
||||||
|
private final String vehicleName;
|
||||||
|
private final String engineMake;
|
||||||
|
private final String engineCode;
|
||||||
|
private final String signature;
|
||||||
|
|
||||||
|
public ControllerInfo(String vehicleName, String engineCode, String engineMake, String signature) {
|
||||||
|
Objects.requireNonNull(vehicleName);
|
||||||
|
Objects.requireNonNull(engineMake);
|
||||||
|
Objects.requireNonNull(engineCode);
|
||||||
|
|
||||||
|
this.vehicleName = vehicleName;
|
||||||
|
this.engineCode = engineCode;
|
||||||
|
this.engineMake = engineMake;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ControllerInfo{" +
|
||||||
|
"vehicleName='" + vehicleName + '\'' +
|
||||||
|
", engineMake='" + engineMake + '\'' +
|
||||||
|
", engineCode='" + engineCode + '\'' +
|
||||||
|
", signature='" + signature + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVehicleName() {
|
||||||
|
return vehicleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEngineMake() {
|
||||||
|
return engineMake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEngineCode() {
|
||||||
|
return engineCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignature() {
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ControllerInfo valueOf(String jsonString) {
|
||||||
|
JsonReader reader = Json.createReader(new StringReader(jsonString));
|
||||||
|
|
||||||
|
JsonObject jsonObject = reader.readObject();
|
||||||
|
String vehicleName = jsonObject.getString(VEHICLE_NAME);
|
||||||
|
String engineMake = jsonObject.getString(ENGINE_MAKE);
|
||||||
|
String engineCode = jsonObject.getString(ENGINE_CODE);
|
||||||
|
String signature = jsonObject.getString(SIGNATURE);
|
||||||
|
|
||||||
|
return new ControllerInfo(vehicleName, engineCode, engineMake, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJson() {
|
||||||
|
JsonObject jsonObject = Json.createObjectBuilder()
|
||||||
|
.add(ENGINE_MAKE, engineMake)
|
||||||
|
.add(ENGINE_CODE, engineCode)
|
||||||
|
.add(VEHICLE_NAME, vehicleName)
|
||||||
|
.add(SIGNATURE, signature)
|
||||||
|
.build();
|
||||||
|
return jsonObject.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
ControllerInfo that = (ControllerInfo) o;
|
||||||
|
return vehicleName.equals(that.vehicleName) &&
|
||||||
|
engineMake.equals(that.engineMake) &&
|
||||||
|
engineCode.equals(that.engineCode) &&
|
||||||
|
signature.equals(that.signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(vehicleName, engineMake, engineCode, signature);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package com.rusefi.server;
|
||||||
|
|
||||||
|
import javax.json.Json;
|
||||||
|
import javax.json.JsonObject;
|
||||||
|
import javax.json.JsonReader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class SessionDetails {
|
||||||
|
public static final String ONE_TIME_TOKEN = "oneTime";
|
||||||
|
public static final String AUTH_TOKEN = "authToken";
|
||||||
|
public static final String CONTROLLER = "controller";
|
||||||
|
|
||||||
|
private final ControllerInfo controllerInfo;
|
||||||
|
|
||||||
|
private final int oneTimeToken;
|
||||||
|
private final String authToken;
|
||||||
|
|
||||||
|
public SessionDetails(ControllerInfo controllerInfo, String authToken, int oneTimeCode) {
|
||||||
|
Objects.requireNonNull(controllerInfo);
|
||||||
|
Objects.requireNonNull(authToken);
|
||||||
|
this.controllerInfo = controllerInfo;
|
||||||
|
this.oneTimeToken = oneTimeCode;
|
||||||
|
this.authToken = authToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int createOneTimeCode() {
|
||||||
|
return new Random().nextInt(60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOneTimeToken() {
|
||||||
|
return oneTimeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerInfo getControllerInfo() {
|
||||||
|
return controllerInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthToken() {
|
||||||
|
return authToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJson() {
|
||||||
|
JsonObject jsonObject = Json.createObjectBuilder()
|
||||||
|
.add(CONTROLLER, controllerInfo.toJson())
|
||||||
|
.add(ONE_TIME_TOKEN, oneTimeToken)
|
||||||
|
.add(AUTH_TOKEN, authToken)
|
||||||
|
.build();
|
||||||
|
return jsonObject.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SessionDetails valueOf(String jsonString) {
|
||||||
|
JsonReader reader = Json.createReader(new StringReader(jsonString));
|
||||||
|
|
||||||
|
JsonObject jsonObject = reader.readObject();
|
||||||
|
String authToken = jsonObject.getString(AUTH_TOKEN);
|
||||||
|
int oneTimeCode = jsonObject.getInt(ONE_TIME_TOKEN);
|
||||||
|
|
||||||
|
ControllerInfo controllerInfo = ControllerInfo.valueOf(jsonObject.getString(CONTROLLER));
|
||||||
|
|
||||||
|
return new SessionDetails(controllerInfo, authToken, oneTimeCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
SessionDetails that = (SessionDetails) o;
|
||||||
|
return oneTimeToken == that.oneTimeToken &&
|
||||||
|
controllerInfo.equals(that.controllerInfo) &&
|
||||||
|
authToken.equals(that.authToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(controllerInfo, oneTimeToken, authToken);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,11 @@ public class UserDetails {
|
||||||
public static final String USER_ID = "user_id";
|
public static final String USER_ID = "user_id";
|
||||||
public static final String USERNAME = "username";
|
public static final String USERNAME = "username";
|
||||||
private final String userName;
|
private final String userName;
|
||||||
private final int id;
|
private final int userId;
|
||||||
|
|
||||||
public UserDetails(String userName, int id) {
|
public UserDetails(String userName, int userId) {
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
this.id = id;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserDetails valueOf(JSONObject element) {
|
public static UserDetails valueOf(JSONObject element) {
|
||||||
|
@ -25,15 +25,15 @@ public class UserDetails {
|
||||||
return userName;
|
return userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getId() {
|
public int getUserId() {
|
||||||
return id;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "UserDetails{" +
|
return "UserDetails{" +
|
||||||
"userName='" + userName + '\'' +
|
"userName='" + userName + '\'' +
|
||||||
", id=" + id +
|
", id=" + userId +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.rusefi.server;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class SessionDetailsTest {
|
||||||
|
@Test
|
||||||
|
public void testSerialization() {
|
||||||
|
ControllerInfo ci = new ControllerInfo("name", "make", "code", "sign");
|
||||||
|
|
||||||
|
SessionDetails sd = new SessionDetails(ci, "auth", 123);
|
||||||
|
|
||||||
|
String json = sd.toJson();
|
||||||
|
|
||||||
|
SessionDetails fromJson = SessionDetails.valueOf(json);
|
||||||
|
|
||||||
|
assertEquals(sd, fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue