113 lines
6.0 KiB
Java
113 lines
6.0 KiB
Java
package com.xtrh.console.service;
|
|||
|
|
|
||
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
|
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||
|
|
import org.junit.jupiter.api.Test;
|
||
|
|
|
||
|
|
import java.util.concurrent.atomic.AtomicLong;
|
||
|
|
|
||
|
|
import static com.xtrh.common.util.Jsons.*;
|
||
|
|
import static org.junit.jupiter.api.Assertions.*;
|
||
|
|
|
||
|
|
class ConsoleCollectionStoreTest {
|
||
|
|
private final AtomicLong time = new AtomicLong(1_000_000);
|
||
|
|
private final ConsoleCollectionStore store = new ConsoleCollectionStore(new ObjectMapper(), time::get);
|
||
|
|
|
||
|
|
@Test void startsEmptyAndBecomesStaleWithoutReplacingLastRealValues() {
|
||
|
|
assertEquals("WAITING", store.snapshot().path("state").asText());
|
||
|
|
assertTrue(store.snapshot().path("lastSyncedAt").isNull());
|
||
|
|
store.sync(input());
|
||
|
|
assertEquals("CONNECTED", store.snapshot().path("state").asText());
|
||
|
|
assertEquals(0, firstItem().path("value").asInt());
|
||
|
|
time.addAndGet(15_001);
|
||
|
|
assertEquals("STALE", store.snapshot().path("state").asText());
|
||
|
|
assertEquals("speed", firstItem().path("key").asText());
|
||
|
|
assertEquals(1_000_000, store.snapshot().path("lastSyncedAt").asLong());
|
||
|
|
ObjectNode fresh = input();
|
||
|
|
fresh.put("observedAt", time.get());
|
||
|
|
store.sync(fresh);
|
||
|
|
assertEquals("CONNECTED", store.snapshot().path("state").asText());
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test void stripsPrivateFieldsAndFiltersSimulatedAndCredentialItems() {
|
||
|
|
ObjectNode input = input().put("token", "must-not-leak");
|
||
|
|
ObjectNode equipment = (ObjectNode) input.path("contexts").get(0).path("equipment").get(0);
|
||
|
|
equipment.put("mediaKey", "must-not-leak");
|
||
|
|
ArrayNode items = (ArrayNode) equipment.path("items");
|
||
|
|
((ObjectNode) items.get(0)).put("keyAlias", "must-not-leak");
|
||
|
|
items.add(item("fake", 50).put("quality", "SIMULATED"));
|
||
|
|
items.add(item("fake2", 50).put("sourceName", "本地模拟"));
|
||
|
|
items.add(item("fake3", 50).put("sourceType", "DEMO"));
|
||
|
|
items.add(item("fake4", 50).put("simulated", true));
|
||
|
|
items.add(item("mediaKey", 123));
|
||
|
|
store.sync(input);
|
||
|
|
assertEquals(1, store.snapshot().path("contexts").get(0).path("equipment").get(0).path("items").size());
|
||
|
|
assertFalse(store.snapshot().toString().contains("must-not-leak"));
|
||
|
|
assertFalse(store.snapshot().toString().contains("mediaKey"));
|
||
|
|
ObjectNode copy = store.snapshot();
|
||
|
|
((ArrayNode) copy.path("contexts")).removeAll();
|
||
|
|
assertEquals(1, store.snapshot().path("contexts").size());
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test void invalidOrOutOfOrderPayloadDoesNotEraseAcceptedSnapshot() {
|
||
|
|
store.sync(input());
|
||
|
|
ObjectNode old = input().put("observedAt", time.get() - 1);
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(old));
|
||
|
|
ObjectNode future = input().put("observedAt", time.get() + 60_001);
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(future));
|
||
|
|
ObjectNode nested = input();
|
||
|
|
((ObjectNode) nested.path("contexts").get(0).path("equipment").get(0).path("items").get(0))
|
||
|
|
.set("value", object().put("secret", "hidden"));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(nested));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(input().put("source", "OTHER")));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(input().put("simulated", true)));
|
||
|
|
assertEquals("speed", firstItem().path("key").asText());
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test void rejectsExcessiveFieldsCountsAndSizeBeforeReplacingState() {
|
||
|
|
store.sync(input());
|
||
|
|
ObjectNode longName = input();
|
||
|
|
((ObjectNode) longName.path("contexts").get(0)).put("name", "x".repeat(201));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(longName));
|
||
|
|
ObjectNode excessive = input();
|
||
|
|
ArrayNode contexts = (ArrayNode) excessive.path("contexts");
|
||
|
|
for (int i = 0; i < 32; i++) contexts.add(object().put("id", "context-" + i).set("equipment", array()));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(excessive));
|
||
|
|
ObjectNode large = input().put("privateExtra", "x".repeat(1_500_000));
|
||
|
|
assertThrows(IllegalArgumentException.class, () -> store.sync(large));
|
||
|
|
assertEquals(1, store.snapshot().path("contexts").size());
|
||
|
|
}
|
||
|
|
|
||
|
|
@Test void nullUnknownQualityAndMissingSignalRemainUnavailableRatherThanBecomingZero() {
|
||
|
|
ObjectNode input = input();
|
||
|
|
ObjectNode item = (ObjectNode) input.path("contexts").get(0).path("equipment").get(0).path("items").get(0);
|
||
|
|
item.putNull("value").putNull("receivedAt").put("quality", "UNRECOGNIZED");
|
||
|
|
store.sync(input);
|
||
|
|
assertTrue(firstItem().path("value").isNull());
|
||
|
|
assertTrue(firstItem().path("receivedAt").isNull());
|
||
|
|
assertEquals("UNKNOWN", firstItem().path("quality").asText());
|
||
|
|
}
|
||
|
|
|
||
|
|
private ObjectNode input() {
|
||
|
|
ObjectNode result = object().put("source", "PXHJ").put("observedAt", time.get());
|
||
|
|
ObjectNode equipment = object().put("id", "equipment-a").put("participantCode", "p-1")
|
||
|
|
.put("name", "真实装备").put("code", "E-01").put("state", "ONLINE").put("lastReceivedAt", time.get());
|
||
|
|
equipment.set("items", array().add(item("speed", 0)));
|
||
|
|
ObjectNode context = object().put("id", "trial-1").put("name", "试验").put("roundId", "round-1");
|
||
|
|
context.set("equipment", array().add(equipment));
|
||
|
|
result.set("contexts", array().add(context));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
private ObjectNode item(String key, int value) {
|
||
|
|
return object().put("key", key).put("label", "速度").put("type", "NUMBER").put("value", value)
|
||
|
|
.put("unit", "m/s").put("receivedAt", time.get()).put("sampleTime", time.get())
|
||
|
|
.put("quality", "GOOD").put("sourceName", "采集模块").put("sourceType", "FIXED_MODULE");
|
||
|
|
}
|
||
|
|
|
||
|
|
private com.fasterxml.jackson.databind.JsonNode firstItem() {
|
||
|
|
return store.snapshot().path("contexts").get(0).path("equipment").get(0).path("items").get(0);
|
||
|
|
}
|
||
|
|
}
|