MapLocationServiceTest.java 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package com.ruoyi.app.map;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONObject;
  4. import com.ruoyi.common.exception.ServiceException;
  5. import com.ruoyi.common.utils.spring.SpringUtils;
  6. import org.junit.jupiter.api.AfterAll;
  7. import org.junit.jupiter.api.BeforeAll;
  8. import org.junit.jupiter.api.BeforeEach;
  9. import org.junit.jupiter.api.Test;
  10. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  11. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  12. import org.springframework.context.support.StaticMessageSource;
  13. import org.springframework.test.util.ReflectionTestUtils;
  14. import java.math.BigDecimal;
  15. import java.util.List;
  16. import java.util.Map;
  17. import static org.junit.jupiter.api.Assertions.*;
  18. import static org.mockito.ArgumentMatchers.*;
  19. import static org.mockito.Mockito.*;
  20. class MapLocationServiceTest {
  21. private static ConfigurableListableBeanFactory originalBeanFactory;
  22. private final MapSourceSelector selector = mock(MapSourceSelector.class);
  23. private final SelfHostedMapClient selfHosted = mock(SelfHostedMapClient.class);
  24. private final GoogleMapLocationClient google = mock(GoogleMapLocationClient.class);
  25. private MapLocationService service;
  26. @BeforeAll
  27. static void messages() {
  28. originalBeanFactory = (ConfigurableListableBeanFactory)
  29. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  30. StaticMessageSource messages = new StaticMessageSource();
  31. messages.setUseCodeAsDefaultMessage(true);
  32. DefaultListableBeanFactory beans = new DefaultListableBeanFactory();
  33. beans.registerSingleton("messageSource", messages);
  34. new SpringUtils().postProcessBeanFactory(beans);
  35. }
  36. @AfterAll
  37. static void restoreMessages() {
  38. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  39. }
  40. @BeforeEach
  41. void setUp() {
  42. service = new MapLocationService(selector, selfHosted, google);
  43. when(selector.selfHostedEnabled()).thenReturn(true);
  44. }
  45. @Test
  46. void searchMapsFlatSelfHostedAddressAndProviderNamespace() {
  47. when(selfHosted.getArray(eq("/search"), anyMap())).thenReturn(JSON.parseArray("""
  48. [{"id":"osm:123","name":"台北車站","address":"臺北市中正區","lat":25.0478,
  49. "lng":121.517,"source":"osm","precision":"poi"}]
  50. """));
  51. MapLocation result = service.search(" 台北車站 ").get(0);
  52. assertEquals("self_hosted:osm:123", result.getId());
  53. assertEquals("台北車站", result.getName());
  54. assertEquals(new BigDecimal("25.0478"), result.getLatitude());
  55. assertEquals("SELF_HOSTED", result.getSource());
  56. assertTrue(result.isRequiresConfirmation());
  57. verifyNoInteractions(google);
  58. }
  59. @Test
  60. void reverseRetainsExactTapInsteadOfNearestCoordinates() {
  61. when(selfHosted.getObject(eq("/reverse_geocode"), anyMap())).thenReturn(JSON.parseObject("""
  62. {"id":"pin:25,121","name":"松智路","address":"松智路","lat":25.03,"lng":121.56,
  63. "precision":"nearby_place","requires_confirmation":true,
  64. "nearest":{"id":"osm:8","lat":25.04,"lng":121.57}}
  65. """));
  66. MapLocation result = service.reverse(new BigDecimal("25.033000123"), new BigDecimal("121.565400987"));
  67. assertEquals(new BigDecimal("25.033000123"), result.getLatitude());
  68. assertEquals(new BigDecimal("121.565400987"), result.getLongitude());
  69. assertEquals("nearby_place", result.getPrecision());
  70. assertTrue(result.isRequiresConfirmation());
  71. }
  72. @Test
  73. void googleSearchAndReverseNormalizeGeometryAndStructuredAddress() {
  74. when(selector.selfHostedEnabled()).thenReturn(false);
  75. JSONObject place = JSON.parseObject("""
  76. {"place_id":"ChIJ123","name":"台北車站","formatted_address":"臺北市中正區",
  77. "geometry":{"location":{"lat":25.0478,"lng":121.517},"location_type":"ROOFTOP"},
  78. "address_components":[{"long_name":"臺灣","types":["country"]},
  79. {"long_name":"臺北市","types":["administrative_area_level_1"]}]}
  80. """);
  81. JSONObject response = new JSONObject();
  82. response.put("status", "OK");
  83. response.put("results", List.of(place));
  84. when(google.getObject(eq("/maps/api/place/textsearch/json"), anyMap())).thenReturn(response);
  85. when(google.getObject(eq("/maps/api/geocode/json"), anyMap())).thenReturn(response);
  86. MapLocation found = service.search("台北").get(0);
  87. assertEquals("google:ChIJ123", found.getId());
  88. assertEquals("GOOGLE", found.getSource());
  89. assertEquals("臺灣", found.getCountry());
  90. assertEquals("臺北市", found.getProvince());
  91. MapLocation reverse = service.reverse(new BigDecimal("25.1"), new BigDecimal("121.6"));
  92. assertEquals(new BigDecimal("25.1"), reverse.getLatitude());
  93. assertEquals(new BigDecimal("121.6"), reverse.getLongitude());
  94. assertEquals("google:pin:25.1,121.6", reverse.getId());
  95. assertEquals("nearby_place", reverse.getPrecision());
  96. assertTrue(reverse.isRequiresConfirmation());
  97. verifyNoInteractions(selfHosted);
  98. }
  99. @Test
  100. void namespacedDetailRemainsResolvableAfterSourceSwitch() {
  101. when(selector.selfHostedEnabled()).thenReturn(false);
  102. when(selfHosted.getObject("/place_detail", Map.of("id", "osm:9"))).thenReturn(JSON.parseObject("""
  103. {"id":"osm:9","name":"門店","lat":25,"lng":121,"precision":"poi"}
  104. """));
  105. assertEquals("self_hosted:osm:9", service.place("self_hosted:osm:9").getId());
  106. verifyNoInteractions(google);
  107. when(selector.selfHostedEnabled()).thenReturn(true);
  108. when(google.getObject("/maps/api/place/details/json", Map.of("place_id", "abc")))
  109. .thenReturn(JSON.parseObject("""
  110. {"status":"OK","result":{"place_id":"abc","name":"店",
  111. "geometry":{"location":{"lat":25,"lng":121}}}}
  112. """));
  113. assertEquals("google:abc", service.place("google:abc").getId());
  114. }
  115. @Test
  116. void nearbyMapsDistanceAndForwardsBoundedRequest() {
  117. when(selfHosted.getArray(eq("/nearby"), anyMap())).thenReturn(JSON.parseArray("""
  118. [{"id":"osm:1","name":"道路","lat":25,"lng":121,
  119. "precision":"street","distance_meters":123.4}]
  120. """));
  121. MapLocation result = service.nearby(BigDecimal.valueOf(25), BigDecimal.valueOf(121), 300, 10).get(0);
  122. assertEquals(new BigDecimal("123.4"), result.getDistanceMeters());
  123. verify(selfHosted).getArray("/nearby", Map.of("lat", BigDecimal.valueOf(25),
  124. "lng", BigDecimal.valueOf(121), "radius", 300, "limit", 10));
  125. }
  126. @Test
  127. void malformedResponsesAndFailureNeverFallBackToGoogle() {
  128. for (String body : List.of("[{\"id\":\"1\",\"name\":\"bad\"}]",
  129. "[{\"id\":\"1\",\"name\":\"bad\",\"lat\":91,\"lng\":121}]", "[null]")) {
  130. when(selfHosted.getArray(eq("/search"), anyMap())).thenReturn(JSON.parseArray(body));
  131. assertEquals("map.location.unavailable", assertThrows(ServiceException.class,
  132. () -> service.search("台北")).getMessage());
  133. }
  134. when(selfHosted.getArray(eq("/search"), anyMap())).thenThrow(new IllegalStateException("offline"));
  135. assertThrows(ServiceException.class, () -> service.search("台北"));
  136. verifyNoInteractions(google);
  137. }
  138. @Test
  139. void googleZeroResultsIsEmptyButDeniedOrMalformedResponseFails() {
  140. when(selector.selfHostedEnabled()).thenReturn(false);
  141. when(google.getObject(anyString(), anyMap())).thenReturn(JSON.parseObject("{\"status\":\"ZERO_RESULTS\"}"));
  142. assertTrue(service.search("unknown").isEmpty());
  143. MapLocation pin = service.reverse(new BigDecimal("25.1"), new BigDecimal("121.6"));
  144. assertEquals("google:pin:25.1,121.6", pin.getId());
  145. assertEquals("coordinate", pin.getPrecision());
  146. assertTrue(pin.isRequiresConfirmation());
  147. for (String body : List.of("{\"status\":\"REQUEST_DENIED\"}", "{\"status\":\"OK\"}",
  148. "{\"status\":\"OK\",\"results\":[{\"place_id\":\"bad\"}]}")) {
  149. when(google.getObject(anyString(), anyMap())).thenReturn(JSON.parseObject(body));
  150. assertThrows(ServiceException.class, () -> service.search("台北"));
  151. }
  152. }
  153. @Test
  154. void invalidRequestsAreRejectedBeforeProviderCall() {
  155. assertThrows(ServiceException.class, () -> service.search(" "));
  156. assertThrows(ServiceException.class, () -> service.search("a".repeat(201)));
  157. assertThrows(ServiceException.class, () -> service.reverse(null, BigDecimal.ONE));
  158. assertThrows(ServiceException.class, () -> service.reverse(BigDecimal.valueOf(91), BigDecimal.ONE));
  159. assertThrows(ServiceException.class, () -> service.nearby(BigDecimal.ONE, BigDecimal.ONE, 2001, 10));
  160. assertThrows(ServiceException.class, () -> service.nearby(BigDecimal.ONE, BigDecimal.ONE, 300, 51));
  161. assertThrows(ServiceException.class, () -> service.place("osm:123"));
  162. verifyNoInteractions(selfHosted, google);
  163. }
  164. }