refactor(接口测试): 重构执行文件接口的响应内容处理

This commit is contained in:
fit2-zhao 2024-05-09 13:53:49 +08:00 committed by Craftsman
parent 1a63411395
commit f4ec65d928
8 changed files with 83 additions and 6 deletions

View File

@ -101,6 +101,11 @@ public class ResponseResult {
*/ */
private long headerSize = 0; private long headerSize = 0;
/**
* 文件地址
*/
private String filePath;
/** /**
* 断言结果 * 断言结果
*/ */

View File

@ -17,6 +17,7 @@ import io.metersphere.system.security.CheckOwner;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -133,5 +134,15 @@ public class ApiTestController {
return apiTestService.getPoolId(projectId); return apiTestService.getPoolId(projectId);
} }
@PostMapping("/download")
@Operation(summary = "执行结果附件下载")
@RequiresPermissions(value = {
PermissionConstants.PROJECT_API_SCENARIO_EXECUTE,
PermissionConstants.PROJECT_API_DEFINITION_CASE_EXECUTE,
PermissionConstants.PROJECT_API_DEBUG_EXECUTE,
PermissionConstants.PROJECT_API_REPORT_READ,
}, logical = Logical.OR)
public void download(@RequestBody TextNode path, HttpServletResponse response) throws Exception {
apiTestService.download(path.asText(), response);
}
} }

View File

@ -11,21 +11,28 @@ import io.metersphere.project.dto.environment.EnvironmentConfig;
import io.metersphere.project.mapper.ExtEnvironmentMapper; import io.metersphere.project.mapper.ExtEnvironmentMapper;
import io.metersphere.project.mapper.ExtProjectMapper; import io.metersphere.project.mapper.ExtProjectMapper;
import io.metersphere.project.service.EnvironmentService; import io.metersphere.project.service.EnvironmentService;
import io.metersphere.project.service.FileService;
import io.metersphere.project.service.ProjectApplicationService; import io.metersphere.project.service.ProjectApplicationService;
import io.metersphere.sdk.constants.ProjectApplicationType; import io.metersphere.sdk.constants.ProjectApplicationType;
import io.metersphere.sdk.constants.StorageType;
import io.metersphere.sdk.domain.Environment; import io.metersphere.sdk.domain.Environment;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.util.BeanUtils; import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.TestResourcePool; import io.metersphere.system.domain.TestResourcePool;
import io.metersphere.system.dto.ProtocolDTO; import io.metersphere.system.dto.ProtocolDTO;
import io.metersphere.system.service.ApiPluginService; import io.metersphere.system.service.ApiPluginService;
import io.metersphere.system.service.PluginLoadService; import io.metersphere.system.service.PluginLoadService;
import io.metersphere.system.utils.ServiceUtils; import io.metersphere.system.utils.ServiceUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginWrapper; import org.pf4j.PluginWrapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -50,6 +57,8 @@ public class ApiTestService {
private ExtProjectMapper extProjectMapper; private ExtProjectMapper extProjectMapper;
@Resource @Resource
private ProjectApplicationService projectApplicationService; private ProjectApplicationService projectApplicationService;
@Resource
private FileService fileService;
public List<ProtocolDTO> getProtocols(String orgId) { public List<ProtocolDTO> getProtocols(String orgId) {
List<ProtocolDTO> protocols = apiPluginService.getProtocols(orgId); List<ProtocolDTO> protocols = apiPluginService.getProtocols(orgId);
@ -122,4 +131,31 @@ public class ApiTestService {
} }
return (String) configMap.get(ProjectApplicationType.API.API_RESOURCE_POOL_ID.name()); return (String) configMap.get(ProjectApplicationType.API.API_RESOURCE_POOL_ID.name());
} }
public void download(String path, HttpServletResponse response) {
if (StringUtils.isBlank(path)) {
return;
}
try {
String fileName = path.substring(path.lastIndexOf("/") + 1);
String folder = path.substring(0, path.lastIndexOf("/"));
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileName);
fileRequest.setFolder(folder);
fileRequest.setStorage(StorageType.MINIO.name());
byte[] bytes = fileService.download(fileRequest);
fileWithResponse(bytes, fileName, response);
} catch (Exception e) {
LogUtils.error(e);
}
}
private void fileWithResponse(byte[] content, String fileName, HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setHeader("Content-disposition", "attachment;filename=" + fileName);
response.getOutputStream().write(content);
response.getOutputStream().flush();
}
} }

View File

@ -245,4 +245,9 @@ public class ApiTestControllerTests extends BaseTest {
projectTestResourcePoolMapper.batchInsert(projectTestResourcePools); projectTestResourcePoolMapper.batchInsert(projectTestResourcePools);
} }
@Test
public void fileDownloadTestSuccess() throws Exception {
this.requestPostAndReturn(BASE_PATH + "download", "test");
}
} }

View File

@ -105,4 +105,10 @@ export function getShareReportInfo(shareId: string) {
export function getShareTime(projectId: string) { export function getShareTime(projectId: string) {
return MSR.get<string>({ url: `${reportUrl.getShareTimeUrl}/${projectId}` }); return MSR.get<string>({ url: `${reportUrl.getShareTimeUrl}/${projectId}` });
} }
// 下载文件
export function downloadFile(data: string | undefined) {
return MSR.post({ url: reportUrl.DownloadFileUrl, data, responseType: 'blob' }, { isTransformResponse: false });
}
export default {}; export default {};

View File

@ -45,3 +45,5 @@ export const reportCaseShareUrl = '/api/report/case/share';
export const getShareIdUrl = '/api/report/share/gen'; export const getShareIdUrl = '/api/report/share/gen';
export const getShareReportInfoUrl = '/api/report/share/get'; export const getShareReportInfoUrl = '/api/report/share/get';
export const getShareTimeUrl = '/api/report/share/get-share-time'; export const getShareTimeUrl = '/api/report/share/get-share-time';
// 下载文件地址
export const DownloadFileUrl = '/api/test/download';

View File

@ -389,6 +389,7 @@ export interface ExecuteRequestParams {
export interface ResponseResult { export interface ResponseResult {
body: string; body: string;
contentType: string; contentType: string;
filePath?: string;
headers: string; headers: string;
dnsLookupTime: number; dnsLookupTime: number;
downloadTime: number; downloadTime: number;

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-if="showImg || isPdf" :class="showType === 'text' ? '' : 'h-full'"> <div v-if="showImg || isPdf || isFile" :class="showType === 'text' ? '' : 'h-full'">
<div class="mb-[8px] flex items-center gap-[16px]"> <div class="mb-[8px] flex items-center gap-[16px]">
<a-button type="outline" class="arco-btn-outline--secondary" size="mini" @click="handleDownload"> <a-button type="outline" class="arco-btn-outline--secondary" size="mini" @click="handleDownload">
{{ t('common.download') }} {{ t('common.download') }}
</a-button> </a-button>
<a-radio-group v-model:model-value="showType" type="button" size="small"> <a-radio-group v-model:model-value="showType" type="button" size="small" :disabled="isFile">
<a-radio v-if="isPdf" value="pdf">pdf</a-radio> <a-radio v-if="isPdf" value="pdf">pdf</a-radio>
<a-radio v-else value="image">{{ t('common.image') }}</a-radio> <a-radio v-else value="image">{{ t('common.image') }}</a-radio>
<a-radio value="text">{{ t('common.text') }}</a-radio> <a-radio value="text">{{ t('common.text') }}</a-radio>
@ -49,8 +49,9 @@
import { LanguageEnum } from '@/components/pure/ms-code-editor/types'; import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { downloadFile } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { downloadUrlFile } from '@/utils'; import { downloadByteFile, downloadUrlFile } from '@/utils';
import { RequestResult } from '@/models/apiTest/common'; import { RequestResult } from '@/models/apiTest/common';
@ -90,6 +91,10 @@
} }
return false; return false;
}); });
const isFile = computed(() => {
const { responseResult } = props.requestResult || {};
return !!responseResult?.filePath;
});
const isPdf = computed(() => { const isPdf = computed(() => {
if (props.requestResult) { if (props.requestResult) {
return props.requestResult.responseResult.contentType === 'application/pdf'; return props.requestResult.responseResult.contentType === 'application/pdf';
@ -117,11 +122,17 @@
} }
}); });
function handleDownload() { async function handleDownload() {
if (isPdf.value) { if (isPdf.value) {
downloadUrlFile(imageUrl.value, 'response.pdf'); downloadUrlFile(imageUrl.value, 'response.pdf');
} else if (imageUrl.value) { } else if (imageUrl.value && !isFile.value) {
downloadUrlFile(imageUrl.value, `response.${props.requestResult?.responseResult.contentType.split('/')[1]}`); downloadUrlFile(imageUrl.value, `response.${props.requestResult?.responseResult.contentType.split('/')[1]}`);
} else {
const res = await downloadFile(props.requestResult?.responseResult.filePath);
const path = props.requestResult?.responseResult.filePath;
const fileName = path?.substring(path.lastIndexOf('/') + 1);
downloadByteFile(res, fileName || 'response.zip');
} }
} }
const responseEditorRef = ref(); const responseEditorRef = ref();