diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/ApiDocController.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/ApiDocController.java index 03690c58828a..2f5cb5ba55c5 100755 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/ApiDocController.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/ApiDocController.java @@ -20,6 +20,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.shenyu.admin.aspect.annotation.RestApi; import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; import org.apache.shenyu.admin.model.result.ShenyuAdminResult; import org.apache.shenyu.admin.model.vo.DocVO; import org.apache.shenyu.admin.model.vo.MenuDocItemVO; @@ -30,6 +31,7 @@ import org.apache.shenyu.admin.service.manager.DocManager; import org.apache.shenyu.common.constant.AdminConstants; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import java.util.Collection; import java.util.List; @@ -83,6 +85,18 @@ public ShenyuAdminResult getAllDoc() { return ShenyuAdminResult.success(docVO); } + /** + * Get document content by the API id returned in the document menu. + * + * @param id API id + * @return document detail + */ + @GetMapping("/getDocItem") + public ShenyuAdminResult getDocItem(@RequestParam("id") final String id) { + DocItem docItem = docManager.getDocItem(id); + return ShenyuAdminResult.success(docItem); + } + private Function getMenuAndDocInfo() { return docInfo -> { MenuProjectVO menuProjectVO = new MenuProjectVO(); @@ -95,6 +109,7 @@ private Function getMenuAndDocInfo() { List docItems = docModule.getDocItems().stream() .map(docItem -> { MenuDocItemVO menuDocItemVO = new MenuDocItemVO(); + menuDocItemVO.setId(docItem.getId()); menuDocItemVO.setLabel(docItem.getSummary()); menuDocItemVO.setName(docItem.getName()); return menuDocItemVO; diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/ApiMapper.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/ApiMapper.java index 38f655c957ca..d75093babf2a 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/ApiMapper.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/ApiMapper.java @@ -63,6 +63,13 @@ public interface ApiMapper extends ExistProvider { */ ApiDO selectByPrimaryKey(String id); + /** + * Select persisted APIs with document content for the document viewer. + * + * @return APIs including their documents + */ + List selectAllWithDocument(); + /** * update record selective. * diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/bean/DocItem.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/bean/DocItem.java index 2f0111d23439..38014f512e17 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/bean/DocItem.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/model/bean/DocItem.java @@ -27,6 +27,8 @@ */ public class DocItem { + private String id; + private String module; private String name; @@ -67,6 +69,24 @@ public class DocItem { private List bizCodeList; + /** + * Get the persisted API id. + * + * @return API id + */ + public String getId() { + return id; + } + + /** + * Set the persisted API id. + * + * @param id API id + */ + public void setId(final String id) { + this.id = id; + } + /** * isUploadRequest. * Whether it is a file upload request. diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/DocManager.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/DocManager.java index dadbee72568f..965c1d160edf 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/DocManager.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/DocManager.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.function.Consumer; import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; import org.apache.shenyu.admin.model.bean.UpstreamInstance; /** @@ -43,4 +44,12 @@ public interface DocManager { * @return Collection */ Collection listAll(); + + /** + * Get a document by its persisted API id. + * + * @param id API id + * @return document item, or null if it is not available + */ + DocItem getDocItem(String id); } diff --git a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImpl.java b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImpl.java index 0e5353eb904f..1c7294a70f42 100644 --- a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImpl.java +++ b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImpl.java @@ -20,21 +20,27 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.TreeMap; import java.util.function.Consumer; +import java.util.stream.Collectors; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.shenyu.admin.mapper.ApiMapper; import org.apache.shenyu.admin.model.bean.DocInfo; import org.apache.shenyu.admin.model.bean.DocItem; import org.apache.shenyu.admin.model.bean.DocModule; import org.apache.shenyu.admin.model.bean.UpstreamInstance; +import org.apache.shenyu.admin.model.entity.ApiDO; import org.apache.shenyu.admin.service.manager.DocManager; import org.apache.shenyu.admin.service.manager.DocParser; import org.apache.shenyu.admin.service.manager.RegisterApiDocService; @@ -65,16 +71,14 @@ public class DocManagerImpl implements DocManager { */ private static final String HTTP = "http://"; - /** - * key:title, value:docInfo. - */ - private static final Map DOC_DEFINITION_MAP = new HashMap<>(); - private static final DocParser SWAGGER_DOC_PARSER = new SwaggerDocParser(); @Resource private RegisterApiDocService registerApiDocService; + @Resource + private ApiMapper apiMapper; + /** * add docInfo. * @@ -179,6 +183,54 @@ private String buildExtJson(final UpstreamInstance instance, final DocItem docIt */ @Override public Collection listAll() { - return DOC_DEFINITION_MAP.values(); + final Map> projects = new TreeMap<>(); + for (ApiDO api : apiMapper.selectAllWithDocument()) { + final DocItem docItem = readDocItem(api); + if (Objects.nonNull(docItem)) { + final String contextPath = StringUtils.defaultIfBlank(api.getContextPath(), "/"); + projects.computeIfAbsent(contextPath, key -> new ArrayList<>()).add(docItem); + } + } + return projects.entrySet().stream().map(entry -> buildDocInfo(entry.getKey(), entry.getValue())).collect(Collectors.toList()); + } + + @Override + public DocItem getDocItem(final String id) { + return StringUtils.isBlank(id) ? null : readDocItem(apiMapper.selectByPrimaryKey(id)); + } + + private DocItem readDocItem(final ApiDO api) { + if (Objects.isNull(api) || StringUtils.isBlank(api.getDocument())) { + return null; + } + try { + final DocItem docItem = GsonUtils.getInstance().fromJson(api.getDocument(), DocItem.class); + // Other API sources may store OpenAPI operations instead of legacy DocItem documents. + if (Objects.isNull(docItem) || StringUtils.isBlank(docItem.getName())) { + return null; + } + docItem.setId(api.getId()); + docItem.setModule(StringUtils.defaultIfBlank(docItem.getModule(), "default")); + return docItem; + } catch (JsonParseException ex) { + LOG.warn("Unable to read API document for id={}", api.getId(), ex); + return null; + } + } + + private DocInfo buildDocInfo(final String contextPath, final List docItems) { + docItems.sort(Comparator.comparingInt(DocItem::getApiOrder).thenComparing(DocItem::getName).thenComparing(DocItem::getId)); + final List modules = docItems.stream().collect(Collectors.groupingBy(DocItem::getModule)).entrySet().stream().map(entry -> { + final DocModule module = new DocModule(); + module.setModule(entry.getKey()); + module.setDocItems(entry.getValue()); + module.setOrder(entry.getValue().stream().mapToInt(DocItem::getModuleOrder).min().orElse(0)); + return module; + }).sorted(Comparator.comparingInt(DocModule::getOrder).thenComparing(DocModule::getModule)).collect(Collectors.toList()); + final DocInfo docInfo = new DocInfo(); + docInfo.setTitle(contextPath); + docInfo.setContextPath(contextPath); + docInfo.setDocModuleList(modules); + return docInfo; } } diff --git a/shenyu-admin/src/main/resources/mappers/api-sqlmap.xml b/shenyu-admin/src/main/resources/mappers/api-sqlmap.xml index 3c1f264603e4..31ebd53fd163 100644 --- a/shenyu-admin/src/main/resources/mappers/api-sqlmap.xml +++ b/shenyu-admin/src/main/resources/mappers/api-sqlmap.xml @@ -57,6 +57,12 @@ delete from api where id = #{id,jdbcType=VARCHAR} + insert into api (id, context_path, api_path, diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/ApiDocControllerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/ApiDocControllerTest.java index 2fedf58a1fcf..2e09944bb41f 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/ApiDocControllerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/ApiDocControllerTest.java @@ -19,6 +19,9 @@ import java.util.ArrayList; import java.util.List; +import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; +import org.apache.shenyu.admin.model.bean.DocModule; import org.apache.shenyu.admin.model.vo.ShenyuDictVO; import org.apache.shenyu.admin.service.ShenyuDictService; import org.apache.shenyu.admin.service.manager.DocManager; @@ -36,6 +39,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -70,4 +76,59 @@ public void testGetAllDoc() throws Exception { .andExpect(status().isOk()) .andReturn(); } + + @Test + public void testMenusIncludePersistedDocumentIds() throws Exception { + DocItem docItem = new DocItem(); + docItem.setId("api-id"); + docItem.setName("/orders/find"); + docItem.setSummary("Find order"); + DocModule module = new DocModule(); + module.setModule("orders"); + module.setDocItems(List.of(docItem)); + DocInfo project = new DocInfo(); + project.setTitle("/orders"); + project.setDocModuleList(List.of(module)); + given(docManager.listAll()).willReturn(List.of(project)); + given(shenyuDictService.list(any())).willReturn(List.of()); + + mockMvc.perform(MockMvcRequestBuilders.get("/apidoc/getDocMenus")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.menuProjects[0].label").value("/orders")) + .andExpect(jsonPath("$.data.menuProjects[0].children[0].children[0].id").value("api-id")) + .andExpect(jsonPath("$.data.menuProjects[0].children[0].children[0].name").value("/orders/find")); + } + + @Test + public void testGetDocItem() throws Exception { + DocItem docItem = new DocItem(); + docItem.setId("api-id"); + docItem.setName("/orders/find"); + docItem.setDescription("Find an order by id"); + given(docManager.getDocItem("api-id")).willReturn(docItem); + + mockMvc.perform(MockMvcRequestBuilders.get("/apidoc/getDocItem").param("id", "api-id")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.id").value("api-id")) + .andExpect(jsonPath("$.data.name").value("/orders/find")) + .andExpect(jsonPath("$.data.description").value("Find an order by id")); + verify(docManager).getDocItem("api-id"); + } + + @Test + public void testGetDocItemRequiresId() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/apidoc/getDocItem")) + .andExpect(status().isBadRequest()); + verify(docManager, never()).getDocItem(any()); + } + + @Test + public void testGetDocItemNotFound() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/apidoc/getDocItem").param("id", "missing")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").doesNotExist()); + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/ApiMapperTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/ApiMapperTest.java index cd5a1dda76da..9d4eff76906f 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/ApiMapperTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/ApiMapperTest.java @@ -18,12 +18,16 @@ package org.apache.shenyu.admin.mapper; import org.apache.shenyu.admin.AbstractSpringIntegrationTest; +import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; import org.apache.shenyu.admin.model.entity.ApiDO; import org.apache.shenyu.admin.model.page.PageParameter; import org.apache.shenyu.admin.model.query.ApiQuery; import org.apache.shenyu.admin.model.page.CommonPager; import org.apache.shenyu.admin.model.vo.ApiVO; import org.apache.shenyu.admin.service.ApiService; +import org.apache.shenyu.admin.service.manager.DocManager; +import org.apache.shenyu.common.utils.JsonUtils; import org.apache.shenyu.common.utils.UUIDUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Test cases for ApiMapper. @@ -51,6 +56,9 @@ public final class ApiMapperTest extends AbstractSpringIntegrationTest { @Resource private ApiService apiService; + @Resource + private DocManager docManager; + private final ApiDO apiDO = buildApiDO(); @BeforeEach @@ -109,6 +117,38 @@ public void testSelectByPrimaryKey() { assertNotNull(apiDO); } + @Test + public void testDocumentViewerReadsPersistedApis() { + ApiDO api = buildApiDO(); + String contextPath = "/document-viewer-" + api.getId(); + api.setContextPath(contextPath); + api.setApiPath(contextPath + "/find"); + DocItem document = new DocItem(); + document.setModule("orders"); + document.setName(api.getApiPath()); + document.setSummary("Persisted document"); + api.setDocument(JsonUtils.toJson(document)); + apiMapper.insert(api); + try { + ApiDO stored = apiMapper.selectAllWithDocument().stream().filter(item -> api.getId().equals(item.getId())).findFirst().orElseThrow(); + assertEquals(api.getDocument(), stored.getDocument()); + DocInfo project = docManager.listAll().stream().filter(item -> contextPath.equals(item.getContextPath())).findFirst().orElseThrow(); + assertEquals(api.getId(), project.getDocModuleList().get(0).getDocItems().get(0).getId()); + assertEquals("Persisted document", docManager.getDocItem(api.getId()).getSummary()); + + document.setSummary("Updated document"); + api.setDocument(JsonUtils.toJson(document)); + apiMapper.updateByPrimaryKeySelective(api); + assertEquals("Updated document", docManager.getDocItem(api.getId()).getSummary()); + + apiMapper.deleteByPrimaryKey(api.getId()); + assertNull(docManager.getDocItem(api.getId())); + assertTrue(docManager.listAll().stream().noneMatch(item -> contextPath.equals(item.getContextPath()))); + } finally { + apiMapper.deleteByPrimaryKey(api.getId()); + } + } + @Test public void testUpdateByPrimaryKeySelective() { Timestamp now = new Timestamp(System.currentTimeMillis()); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java index 503aa4f7f754..570b9b05ba7d 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/impl/SwaggerImportServiceImplTest.java @@ -25,6 +25,7 @@ import okio.Buffer; import okio.BufferedSource; import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; import org.apache.shenyu.admin.model.bean.UpstreamInstance; import org.apache.shenyu.admin.model.dto.SwaggerImportRequest; import org.apache.shenyu.admin.service.manager.DocManager; @@ -221,6 +222,11 @@ public Collection listAll() { return Collections.emptyList(); } + @Override + public DocItem getDocItem(final String id) { + return null; + } + private String getDocJson() { return docJson; } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImplTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImplTest.java index 60dfdd7dfc32..170451c11bb7 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImplTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/impl/DocManagerImplTest.java @@ -17,8 +17,14 @@ package org.apache.shenyu.admin.service.manager.impl; +import org.apache.shenyu.admin.mapper.ApiMapper; +import org.apache.shenyu.admin.model.bean.DocInfo; +import org.apache.shenyu.admin.model.bean.DocItem; +import org.apache.shenyu.admin.model.bean.DocParameter; import org.apache.shenyu.admin.model.bean.UpstreamInstance; +import org.apache.shenyu.admin.model.entity.ApiDO; import org.apache.shenyu.admin.service.manager.RegisterApiDocService; +import org.apache.shenyu.common.utils.JsonUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,8 +34,17 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + /** * test cases for DocManagerImpl. */ @@ -42,6 +57,9 @@ public class DocManagerImplTest { @Mock private RegisterApiDocService registerApiDocService; + @Mock + private ApiMapper apiMapper; + @Test public void testAddDocInfo() { UpstreamInstance instance = new UpstreamInstance(); @@ -54,5 +72,117 @@ public void testAddDocInfo() { }); Assertions.assertTrue(atomicBoolean.get()); + verify(registerApiDocService, atLeastOnce()).registerApiDocument(any()); + verifyNoInteractions(apiMapper); + } + + @Test + public void testListAllReadsPersistedDocuments() { + ApiDO orders = buildApi("orders-id", "/orders", "orders", "/orders/find"); + ApiDO payments = buildApi("payments-id", "/payments", "payments", "/payments/find"); + when(apiMapper.selectAllWithDocument()).thenReturn(List.of(payments, orders)); + + List projects = new ArrayList<>(docManager.listAll()); + + Assertions.assertEquals(2, projects.size()); + Assertions.assertEquals("/orders", projects.get(0).getTitle()); + Assertions.assertEquals("/orders", projects.get(0).getContextPath()); + DocItem menuItem = projects.get(0).getDocModuleList().get(0).getDocItems().get(0); + Assertions.assertEquals("orders-id", menuItem.getId()); + Assertions.assertEquals("/orders/find", menuItem.getName()); + Assertions.assertEquals("/payments", projects.get(1).getTitle()); + verifyNoInteractions(registerApiDocService); + } + + @Test + public void testGetDocItemReturnsStoredContentAndDatabaseId() { + ApiDO api = buildApi("api-id", "/orders", "orders", "/orders/find"); + when(apiMapper.selectByPrimaryKey("api-id")).thenReturn(api); + + DocItem docItem = docManager.getDocItem("api-id"); + + Assertions.assertEquals("api-id", docItem.getId()); + Assertions.assertEquals("/orders/find", docItem.getName()); + Assertions.assertEquals("Find an order", docItem.getSummary()); + Assertions.assertEquals(List.of("get"), new ArrayList<>(docItem.getHttpMethodList())); + Assertions.assertEquals("orderId", docItem.getRequestParameters().get(0).getName()); + verifyNoInteractions(registerApiDocService); + } + + @Test + public void testGetDocItemMissingOrBlank() { + Assertions.assertNull(docManager.getDocItem("missing")); + Assertions.assertNull(docManager.getDocItem(null)); + Assertions.assertNull(docManager.getDocItem(" ")); + verify(apiMapper).selectByPrimaryKey("missing"); + } + + @Test + public void testInvalidDocumentsDoNotBreakMenusOrDetail() { + List apis = new ArrayList<>(); + for (String document : List.of("", " ", "not-json", "null", "{}", "{\"operationId\":\"/orders/find\"}")) { + ApiDO api = new ApiDO(); + api.setId("invalid-" + apis.size()); + api.setDocument(document); + apis.add(api); + when(apiMapper.selectByPrimaryKey(api.getId())).thenReturn(api); + Assertions.assertNull(docManager.getDocItem(api.getId())); + } + ApiDO valid = buildApi("valid-id", "/orders", "orders", "/orders/find"); + apis.add(valid); + when(apiMapper.selectAllWithDocument()).thenReturn(apis); + + List projects = new ArrayList<>(docManager.listAll()); + + Assertions.assertEquals(1, projects.size()); + Assertions.assertEquals("valid-id", projects.get(0).getDocModuleList().get(0).getDocItems().get(0).getId()); + } + + @Test + public void testListAllPreservesModuleAndApiOrdering() { + ApiDO first = buildApi("first-id", "/orders", "first", "/orders/b"); + ApiDO second = buildApi("second-id", "/orders", "second", "/orders/a"); + ApiDO third = buildApi("third-id", "/orders", "first", "/orders/a"); + DocItem secondItem = JsonUtils.jsonToObject(second.getDocument(), DocItem.class); + secondItem.setModuleOrder(1); + second.setDocument(JsonUtils.toJson(secondItem)); + when(apiMapper.selectAllWithDocument()).thenReturn(List.of(second, first, third)); + + DocInfo project = docManager.listAll().iterator().next(); + + Assertions.assertEquals("first", project.getDocModuleList().get(0).getModule()); + Assertions.assertEquals("second", project.getDocModuleList().get(1).getModule()); + Assertions.assertEquals("third-id", project.getDocModuleList().get(0).getDocItems().get(0).getId()); + Assertions.assertEquals("first-id", project.getDocModuleList().get(0).getDocItems().get(1).getId()); + } + + @Test + public void testListAllHandlesMissingModuleAndContextPath() { + ApiDO api = buildApi("api-id", null, null, "/orders/find"); + when(apiMapper.selectAllWithDocument()).thenReturn(List.of(api)); + + DocInfo project = docManager.listAll().iterator().next(); + + Assertions.assertEquals("/", project.getContextPath()); + Assertions.assertEquals("default", project.getDocModuleList().get(0).getModule()); + } + + private ApiDO buildApi(final String id, final String contextPath, final String module, final String path) { + DocItem docItem = new DocItem(); + docItem.setId("stale-document-id"); + docItem.setName(path); + docItem.setModule(module); + docItem.setSummary("Find an order"); + docItem.setHttpMethodList(Set.of("get")); + DocParameter parameter = new DocParameter(); + parameter.setName("orderId"); + parameter.setType("string"); + docItem.setRequestParameters(List.of(parameter)); + ApiDO api = new ApiDO(); + api.setId(id); + api.setContextPath(contextPath); + api.setApiPath(path); + api.setDocument(JsonUtils.toJson(docItem)); + return api; } }