diff --git a/src/main/java/ua/ndmik/bot/client/DtekCookieProvider.java b/src/main/java/ua/ndmik/bot/client/DtekCookieProvider.java index 130f931..3d9a73d 100644 --- a/src/main/java/ua/ndmik/bot/client/DtekCookieProvider.java +++ b/src/main/java/ua/ndmik/bot/client/DtekCookieProvider.java @@ -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()); } } @@ -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; } @@ -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; } diff --git a/src/main/java/ua/ndmik/bot/client/YasnoClient.java b/src/main/java/ua/ndmik/bot/client/YasnoClient.java index 3486d65..3f20a60 100644 --- a/src/main/java/ua/ndmik/bot/client/YasnoClient.java +++ b/src/main/java/ua/ndmik/bot/client/YasnoClient.java @@ -89,29 +89,39 @@ private List 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; } @@ -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"), @@ -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 ""; } } @@ -147,7 +161,7 @@ private String executeRequest(Supplier 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 ""; } } @@ -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 ""; diff --git a/src/test/java/ua/ndmik/bot/client/YasnoClientTests.java b/src/test/java/ua/ndmik/bot/client/YasnoClientTests.java new file mode 100644 index 0000000..b100c17 --- /dev/null +++ b/src/test/java/ua/ndmik/bot/client/YasnoClientTests.java @@ -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 streets = client.findStreets(25, 902, "хре"); + + assertThat(streets).containsExactly(new AddressItem(1064L, "вул. Хрещатик")); + } + + @Test + void findStreets_parsesXssiPrefixedPayload() { + YasnoClient client = clientWithResponse(")]}',\n[{\"id\":1064,\"value\":\"вул. Хрещатик\"}]"); + + List streets = client.findStreets(25, 902, "хре"); + + assertThat(streets).containsExactly(new AddressItem(1064L, "вул. Хрещатик")); + } + + @Test + void findStreets_ignoresNonJsonPayload() { + YasnoClient client = clientWithResponse("blocked"); + + List 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); + } +}