Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/main/java/ua/ndmik/bot/client/DtekCookieProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ private void navigateForCookies(Page page, DtekArea area) {
page.waitForLoadState(LoadState.DOMCONTENTLOADED,
new Page.WaitForLoadStateOptions().setTimeout(DOM_CONTENT_LOADED_WAIT_MS));
} catch (PlaywrightException e) {
log.debug("DOM content loaded was not reached quickly while retrieving cookies: {}", e.getMessage());
log.warn("DOM content loaded was not reached quickly while retrieving cookies: {}", e.getMessage());
}
try {
page.waitForLoadState(LoadState.NETWORKIDLE,
new Page.WaitForLoadStateOptions().setTimeout(NETWORK_IDLE_WAIT_MS));
} catch (PlaywrightException e) {
log.debug("Network idle was not reached quickly while retrieving cookies: {}", e.getMessage());
log.warn("Network idle was not reached quickly while retrieving cookies: {}", e.getMessage());
}
}

Expand All @@ -124,7 +124,7 @@ private void closeResources() {
try {
browser.close();
} catch (RuntimeException e) {
log.debug("Failed to close Playwright browser cleanly", e);
log.warn("Failed to close Playwright browser cleanly", e);
} finally {
browser = null;
}
Expand All @@ -133,7 +133,7 @@ private void closeResources() {
try {
playwright.close();
} catch (RuntimeException e) {
log.debug("Failed to close Playwright cleanly", e);
log.warn("Failed to close Playwright cleanly", e);
} finally {
playwright = null;
}
Expand Down
141 changes: 124 additions & 17 deletions src/main/java/ua/ndmik/bot/client/YasnoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,39 @@ private List<AddressItem> parseAddressItems(String json) {
}

try {
JsonNode root = mapper.readTree(json);
JsonNode itemsNode = root.isArray()
? root
: root.path("data");
JsonNode root = readJsonRoot(json);
if (root == null) {
log.warn("Unexpected non-JSON YASNO address payload: {}", snippet(json));
return result;
}

JsonNode itemsNode = firstArray(
root,
root.path("data"),
root.path("result"),
root.path("items"),
root.path("data").path("items")
);
if (!itemsNode.isArray()) {
log.debug("Unexpected YASNO address payload: {}", json);
log.warn("Unexpected YASNO address payload: {}", json);
return result;
}

for (JsonNode item : itemsNode) {
long id = item.path("id").asLong();
String name = item.path("name").asString();
String fullName = item.path("fullName").asString();
String displayName = !fullName.isBlank()
? fullName
: name;
if (id > 0) {
long id = firstLong(item.path("id"), item.path("streetId"), item.path("houseId"));
String displayName = firstText(
item.path("fullName"),
item.path("name"),
item.path("value"),
item.path("label")
);
if (id > 0 && !displayName.isBlank()) {
result.add(new AddressItem(id, displayName));
}
}
} catch (RuntimeException ex) {
log.warn("Failed to parse YASNO address payload");
log.debug("YASNO address payload parse error: {}", json, ex);
log.warn("YASNO address payload parse error: {}", json, ex);
}
return result;
}
Expand All @@ -122,7 +132,11 @@ private String extractGroupId(String json) {
}

try {
JsonNode root = mapper.readTree(json);
JsonNode root = readJsonRoot(json);
if (root == null) {
log.warn("Unexpected non-JSON YASNO group payload: {}", snippet(json));
return "";
}
JsonNode candidate = firstPresent(
root.path("group"),
root.path("groupId"),
Expand All @@ -131,13 +145,13 @@ private String extractGroupId(String json) {
);

if (candidate == null || candidate.isMissingNode() || candidate.isNull()) {
log.debug("Unexpected YASNO group payload: {}", json);
log.warn("Unexpected YASNO group payload: {}", json);
return "";
}
return text(candidate).trim();
} catch (RuntimeException ex) {
log.warn("Failed to parse YASNO group payload");
log.debug("YASNO group payload parse error: {}", json, ex);
log.warn("YASNO group payload parse error: {}", json, ex);
return "";
}
}
Expand All @@ -147,7 +161,7 @@ private String executeRequest(Supplier<String> request, String operation, String
return Objects.toString(request.get(), "");
} catch (RuntimeException ex) {
log.warn("YASNO {} request failed ({})", operation, params);
log.debug("YASNO {} request error", operation, ex);
log.warn("YASNO {} request error", operation, ex);
return "";
}
}
Expand All @@ -165,6 +179,99 @@ private JsonNode firstPresent(JsonNode... candidates) {
return null;
}

private JsonNode firstArray(JsonNode... candidates) {
for (JsonNode candidate : candidates) {
if (candidate != null && candidate.isArray()) {
return candidate;
}
}
return null;
}

private long firstLong(JsonNode... candidates) {
for (JsonNode candidate : candidates) {
if (candidate == null || candidate.isMissingNode() || candidate.isNull()) {
continue;
}
long value = candidate.asLong(0L);
if (value > 0L) {
return value;
}
}
return 0L;
}

private String firstText(JsonNode... candidates) {
for (JsonNode candidate : candidates) {
String value = text(candidate);
if (!value.isBlank()) {
return value;
}
}
return "";
}

private JsonNode readJsonRoot(String payload) {
String normalized = normalizeJson(payload);
if (normalized.isBlank()) {
return null;
}
return mapper.readTree(normalized);
}

private String normalizeJson(String payload) {
if (payload == null) {
return "";
}
String trimmed = payload.stripLeading();
if (trimmed.isBlank()) {
return "";
}

char first = trimmed.charAt(0);
if (first == '{' || first == '[') {
return trimmed;
}
if (first == '<') {
return "";
}

int objectStart = trimmed.indexOf('{');
int arrayStart = trimmed.indexOf('[');
int startIndex = selectStartIndex(objectStart, arrayStart);
if (startIndex < 0) {
return "";
}

String prefix = trimmed.substring(0, startIndex).trim();
if (prefix.startsWith(")]}',") || prefix.startsWith("for(;;);")) {
return trimmed.substring(startIndex);
}
return "";
}

private int selectStartIndex(int objectStart, int arrayStart) {
if (objectStart < 0) {
return arrayStart;
}
if (arrayStart < 0) {
return objectStart;
}
return Math.min(objectStart, arrayStart);
}

private String snippet(String payload) {
if (payload == null) {
return "";
}
String normalized = payload.replaceAll("\\s+", " ").trim();
int maxLength = 240;
if (normalized.length() <= maxLength) {
return normalized;
}
return normalized.substring(0, maxLength) + "...";
}

private String text(JsonNode node) {
if (node == null || node.isMissingNode() || node.isNull()) {
return "";
Expand Down
64 changes: 64 additions & 0 deletions src/test/java/ua/ndmik/bot/client/YasnoClientTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ua.ndmik.bot.client;

import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestClient;
import ua.ndmik.bot.model.yasno.AddressItem;

import java.util.List;
import java.util.function.Function;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;

class YasnoClientTests {

@Test
void findStreets_parsesValueFieldFromCurrentApiSchema() {
YasnoClient client = clientWithResponse("[{\"id\":1064,\"value\":\"вул. Хрещатик\"}]");

List<AddressItem> streets = client.findStreets(25, 902, "хре");

assertThat(streets).containsExactly(new AddressItem(1064L, "вул. Хрещатик"));
}

@Test
void findStreets_parsesXssiPrefixedPayload() {
YasnoClient client = clientWithResponse(")]}',\n[{\"id\":1064,\"value\":\"вул. Хрещатик\"}]");

List<AddressItem> streets = client.findStreets(25, 902, "хре");

assertThat(streets).containsExactly(new AddressItem(1064L, "вул. Хрещатик"));
}

@Test
void findStreets_ignoresNonJsonPayload() {
YasnoClient client = clientWithResponse("<html>blocked</html>");

List<AddressItem> streets = client.findStreets(25, 902, "хре");

assertThat(streets).isEmpty();
}

@Test
void findGroup_extractsNumericGroup() {
YasnoClient client = clientWithResponse("{\"group\":20,\"subgroup\":1}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("20");
}

@SuppressWarnings("unchecked")
private YasnoClient clientWithResponse(String responseBody) {
RestClient restClient = mock(RestClient.class, RETURNS_DEEP_STUBS);
given(restClient.get()
.uri(any(Function.class))
.retrieve()
.body(String.class))
.willReturn(responseBody);
return new YasnoClient(restClient);
}
}
Loading