mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
🐛 fix: 修复 GitHub Actions 工作流 YAML 语法错误
- 修复 dependencies.yml 中多行字符串缩进不一致的问题 - 将 commit-message 改为单行字符串格式 - 修复 reviewers 字段的空值语法 - 确保所有 YAML 文件符合严格的语法规范 - 通过 prettier 格式化所有工作流文件
This commit is contained in:
@@ -314,13 +314,8 @@ jobs:
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
🔄 chore: update dependencies (${{ github.event.inputs.update_type || 'patch' }})
|
||||
|
||||
- Updated ${{ github.event.inputs.update_type || 'patch' }} versions of dependencies
|
||||
- All tests pass with updated dependencies
|
||||
- Security audit: ${{ needs.check-updates.outputs.update-summary }}
|
||||
title: '🔄 Dependency Updates (${{ github.event.inputs.update_type || 'patch' }})'
|
||||
commit-message: "🔄 chore: update dependencies (${{ github.event.inputs.update_type || 'patch' }})"
|
||||
title: "🔄 Dependency Updates (${{ github.event.inputs.update_type || 'patch' }})"
|
||||
body: |
|
||||
## 🔄 Automated Dependency Update
|
||||
|
||||
@@ -351,8 +346,7 @@ jobs:
|
||||
dependencies
|
||||
automated
|
||||
${{ github.event.inputs.update_type || 'patch' }}
|
||||
reviewers: |
|
||||
# 添加默认审查者
|
||||
reviewers: ''
|
||||
draft: false
|
||||
|
||||
- name: ❌ Report failed update
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# 每天凌晨 2 点运行质量检查
|
||||
- cron: "0 2 * * *"
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
# ============================================================================
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -61,8 +61,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -90,8 +90,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -160,8 +160,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -257,8 +257,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -292,15 +292,7 @@ jobs:
|
||||
quality-summary:
|
||||
name: 📋 Quality Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
formatting,
|
||||
typescript,
|
||||
dependencies,
|
||||
complexity,
|
||||
documentation,
|
||||
benchmarks,
|
||||
]
|
||||
needs: [formatting, typescript, dependencies, complexity, documentation, benchmarks]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
|
||||
@@ -3,26 +3,26 @@ name: 🚀 Release & Deploy
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- 'v*'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version (e.g., v1.0.0)"
|
||||
description: 'Release version (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
environment:
|
||||
description: "Deployment environment"
|
||||
description: 'Deployment environment'
|
||||
required: true
|
||||
default: "staging"
|
||||
default: 'staging'
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18"
|
||||
NODE_VERSION: '18'
|
||||
|
||||
jobs:
|
||||
# ============================================================================
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
|
||||
+16
-24
@@ -27,8 +27,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -96,8 +96,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -133,8 +133,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -170,8 +170,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -235,8 +235,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: |
|
||||
@@ -279,8 +279,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -312,8 +312,8 @@ jobs:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📥 Install dependencies
|
||||
run: npm ci
|
||||
@@ -336,15 +336,7 @@ jobs:
|
||||
test-summary:
|
||||
name: 📋 Test Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
unit-tests,
|
||||
integration-tests,
|
||||
performance-tests,
|
||||
coverage,
|
||||
browser-tests,
|
||||
build-test,
|
||||
]
|
||||
needs: [unit-tests, integration-tests, performance-tests, coverage, browser-tests, build-test]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
|
||||
+170
-147
@@ -39,12 +39,21 @@
|
||||
</div>
|
||||
|
||||
<!-- 文件夹读取 loading -->
|
||||
<n-progress v-if="isFolderLoading" :percentage="folderLoadingProgress" type="line" :show-indicator="true" style="margin-bottom: 16px">
|
||||
<n-progress
|
||||
v-if="isFolderLoading"
|
||||
:percentage="folderLoadingProgress"
|
||||
type="line"
|
||||
:show-indicator="true"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #default> {{ t('Reading Folder Files') }} ({{ folderLoadingProgress }}%) </template>
|
||||
</n-progress>
|
||||
|
||||
<div @dragover.prevent @drop.prevent="handleDrop">
|
||||
<div v-if="selectedItems.length === 0" class="flex flex-col gap-6 items-center justify-center border border-dashed border-muted rounded p-4 h-[40vh]">
|
||||
<div
|
||||
v-if="selectedItems.length === 0"
|
||||
class="flex flex-col gap-6 items-center justify-center border border-dashed border-muted rounded p-4 h-[40vh]"
|
||||
>
|
||||
<div class="text-gray-500 font-bold">{{ t('No Selection') }}</div>
|
||||
<div class="text-muted-foreground text-center">
|
||||
{{ t('Drag Drop Info') }}<br />
|
||||
@@ -53,7 +62,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 虚拟滚动文件与文件夹列表 -->
|
||||
<n-virtual-list v-if="selectedItems.length" :items="selectedItems" :item-size="60" class="h-[40vh] overflow-y-auto border rounded">
|
||||
<n-virtual-list
|
||||
v-if="selectedItems.length"
|
||||
:items="selectedItems"
|
||||
:item-size="60"
|
||||
class="h-[40vh] overflow-y-auto border rounded"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div class="flex items-center gap-2 w-full border-b p-1">
|
||||
<div class="flex-1">
|
||||
@@ -74,15 +88,25 @@
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
|
||||
<n-progress v-if="isAdding" :percentage="addProgress" type="line" :show-indicator="true" style="margin-bottom: 16px">
|
||||
<n-progress
|
||||
v-if="isAdding"
|
||||
:percentage="addProgress"
|
||||
type="line"
|
||||
:show-indicator="true"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #default> {{ t('Adding to Upload Queue') }} ({{ addProgress }}%) </template>
|
||||
</n-progress>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<n-button type="default" :disabled="!hasFiles || isAdding || isFolderLoading">{{ t('Configure') }}</n-button>
|
||||
<n-button type="primary" :disabled="!hasFiles || isAdding || isFolderLoading" :loading="isAdding" @click="handleUpload">{{
|
||||
t('Start Upload')
|
||||
}}</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!hasFiles || isAdding || isFolderLoading"
|
||||
:loading="isAdding"
|
||||
@click="handleUpload"
|
||||
>{{ t('Start Upload') }}</n-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
@@ -90,121 +114,121 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks';
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore()
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore();
|
||||
|
||||
interface FileItem {
|
||||
file: File
|
||||
prefix: string
|
||||
file: File;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
interface OptimizedFileGroup {
|
||||
files: File[]
|
||||
totalSize: number
|
||||
fileCount: number
|
||||
files: File[];
|
||||
totalSize: number;
|
||||
fileCount: number;
|
||||
}
|
||||
|
||||
interface SelectedItem {
|
||||
type: 'file' | 'folder'
|
||||
name: string
|
||||
size: number
|
||||
fileCount?: number
|
||||
files?: FileItem[]
|
||||
type: 'file' | 'folder';
|
||||
name: string;
|
||||
size: number;
|
||||
fileCount?: number;
|
||||
files?: FileItem[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:show', 'submit'])
|
||||
const emit = defineEmits(['update:show', 'submit']);
|
||||
|
||||
const props = defineProps<{ show: boolean; bucketName: string; prefix: string }>()
|
||||
const props = defineProps<{ show: boolean; bucketName: string; prefix: string }>();
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const folderInput = ref<HTMLInputElement | null>(null)
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const folderInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 待上传的文件/文件夹列表
|
||||
const selectedItems = ref<SelectedItem[]>([])
|
||||
const selectedItems = ref<SelectedItem[]>([]);
|
||||
|
||||
const isAdding = ref(false)
|
||||
const addProgress = ref(0)
|
||||
const isFolderLoading = ref(false)
|
||||
const folderLoadingProgress = ref(0)
|
||||
const isAdding = ref(false);
|
||||
const addProgress = ref(0);
|
||||
const isFolderLoading = ref(false);
|
||||
const folderLoadingProgress = ref(0);
|
||||
|
||||
// 内存优化配置
|
||||
const MAX_FILES_LIMIT = 10000 // 最大文件数限制
|
||||
const MEMORY_WARNING_THRESHOLD = 5000 // 内存警告阈值
|
||||
const isMemoryWarning = ref(false)
|
||||
const totalFileCount = ref(0)
|
||||
const MAX_FILES_LIMIT = 10000; // 最大文件数限制
|
||||
const MEMORY_WARNING_THRESHOLD = 5000; // 内存警告阈值
|
||||
const isMemoryWarning = ref(false);
|
||||
const totalFileCount = ref(0);
|
||||
|
||||
// 清理内存函数
|
||||
const clearAllFiles = () => {
|
||||
selectedItems.value = []
|
||||
totalFileCount.value = 0
|
||||
isMemoryWarning.value = false
|
||||
selectedItems.value = [];
|
||||
totalFileCount.value = 0;
|
||||
isMemoryWarning.value = false;
|
||||
|
||||
// 强制垃圾回收(如果支持)
|
||||
if (window.gc) {
|
||||
window.gc()
|
||||
window.gc();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
// 关闭时清理内存
|
||||
clearAllFiles()
|
||||
emit('update:show', false)
|
||||
}
|
||||
clearAllFiles();
|
||||
emit('update:show', false);
|
||||
};
|
||||
|
||||
const selectFile = () => fileInput.value?.click()
|
||||
const selectFolder = () => folderInput.value?.click()
|
||||
const selectFile = () => fileInput.value?.click();
|
||||
const selectFolder = () => folderInput.value?.click();
|
||||
|
||||
// 内存监控
|
||||
const checkMemoryUsage = () => {
|
||||
if ('memory' in performance) {
|
||||
const memInfo = (performance as any).memory
|
||||
const usedMB = memInfo.usedJSHeapSize / 1024 / 1024
|
||||
const memInfo = (performance as any).memory;
|
||||
const usedMB = memInfo.usedJSHeapSize / 1024 / 1024;
|
||||
|
||||
// 如果内存使用超过 100MB 且文件数量很大,建议清理
|
||||
if (usedMB > 100 && totalFileCount.value > MEMORY_WARNING_THRESHOLD) {
|
||||
message.warning(
|
||||
t('High Memory Usage Warning', {
|
||||
memory: Math.round(usedMB),
|
||||
files: totalFileCount.value
|
||||
files: totalFileCount.value,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files) {
|
||||
const fileCount = input.files.length
|
||||
const currentTotal = totalFileCount.value + fileCount
|
||||
const fileCount = input.files.length;
|
||||
const currentTotal = totalFileCount.value + fileCount;
|
||||
|
||||
// 检查文件数量限制
|
||||
if (currentTotal > MAX_FILES_LIMIT) {
|
||||
message.error(
|
||||
t('File Count Limit Exceeded', {
|
||||
current: currentTotal,
|
||||
max: MAX_FILES_LIMIT
|
||||
max: MAX_FILES_LIMIT,
|
||||
})
|
||||
)
|
||||
input.value = ''
|
||||
return
|
||||
);
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 内存警告
|
||||
if (currentTotal > MEMORY_WARNING_THRESHOLD && !isMemoryWarning.value) {
|
||||
isMemoryWarning.value = true
|
||||
isMemoryWarning.value = true;
|
||||
message.warning(
|
||||
t('Memory Warning', {
|
||||
count: currentTotal,
|
||||
threshold: MEMORY_WARNING_THRESHOLD
|
||||
threshold: MEMORY_WARNING_THRESHOLD,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 更新展示列表
|
||||
@@ -215,83 +239,83 @@ const handleFileSelect = (e: Event) => {
|
||||
size: f.size,
|
||||
files: [{ file: f, prefix: props.prefix }],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// 更新总文件数
|
||||
totalFileCount.value = currentTotal
|
||||
totalFileCount.value = currentTotal;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderSelect = async (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const input = e.target as HTMLInputElement;
|
||||
|
||||
if (!input.files || input.files.length === 0) return
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const fileCount = input.files.length
|
||||
const currentTotal = totalFileCount.value + fileCount
|
||||
const fileCount = input.files.length;
|
||||
const currentTotal = totalFileCount.value + fileCount;
|
||||
|
||||
// 检查文件数量限制
|
||||
if (currentTotal > MAX_FILES_LIMIT) {
|
||||
message.error(
|
||||
t('File Count Limit Exceeded', {
|
||||
current: currentTotal,
|
||||
max: MAX_FILES_LIMIT
|
||||
max: MAX_FILES_LIMIT,
|
||||
})
|
||||
)
|
||||
input.value = ''
|
||||
return
|
||||
);
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 内存警告
|
||||
if (currentTotal > MEMORY_WARNING_THRESHOLD && !isMemoryWarning.value) {
|
||||
isMemoryWarning.value = true
|
||||
isMemoryWarning.value = true;
|
||||
message.warning(
|
||||
t('Memory Warning', {
|
||||
count: currentTotal,
|
||||
threshold: MEMORY_WARNING_THRESHOLD
|
||||
threshold: MEMORY_WARNING_THRESHOLD,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 开始 loading
|
||||
isFolderLoading.value = true
|
||||
folderLoadingProgress.value = 0
|
||||
isFolderLoading.value = true;
|
||||
folderLoadingProgress.value = 0;
|
||||
|
||||
try {
|
||||
const folderFiles = Array.from(input.files)
|
||||
const folderMap = new Map<string, OptimizedFileGroup>()
|
||||
const batchSize = 200 // 增加批处理大小以提高性能
|
||||
const folderFiles = Array.from(input.files);
|
||||
const folderMap = new Map<string, OptimizedFileGroup>();
|
||||
const batchSize = 200; // 增加批处理大小以提高性能
|
||||
|
||||
// 分批处理文件,避免阻塞主线程
|
||||
for (let i = 0; i < folderFiles.length; i += batchSize) {
|
||||
const batch = folderFiles.slice(i, i + batchSize)
|
||||
const batch = folderFiles.slice(i, i + batchSize);
|
||||
|
||||
// 处理当前批次的文件
|
||||
for (const file of batch) {
|
||||
// 保留完整目录路径(去掉最后的文件名)
|
||||
const pathParts = file.webkitRelativePath.split('/')
|
||||
pathParts.pop() // 移除文件名
|
||||
const folderPath = pathParts.join('/') || t('Unknown Folder')
|
||||
const pathParts = file.webkitRelativePath.split('/');
|
||||
pathParts.pop(); // 移除文件名
|
||||
const folderPath = pathParts.join('/') || t('Unknown Folder');
|
||||
|
||||
if (!folderMap.has(folderPath)) {
|
||||
folderMap.set(folderPath, {
|
||||
files: [],
|
||||
totalSize: 0,
|
||||
fileCount: 0
|
||||
})
|
||||
fileCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const group = folderMap.get(folderPath)!
|
||||
group.files.push(file)
|
||||
group.totalSize += file.size
|
||||
group.fileCount++
|
||||
const group = folderMap.get(folderPath)!;
|
||||
group.files.push(file);
|
||||
group.totalSize += file.size;
|
||||
group.fileCount++;
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
folderLoadingProgress.value = Math.round(((i + batch.length) / folderFiles.length) * 100)
|
||||
folderLoadingProgress.value = Math.round(((i + batch.length) / folderFiles.length) * 100);
|
||||
|
||||
// 让出主线程,保证 UI 可响应
|
||||
await new Promise(resolve => setTimeout(resolve, 1))
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
}
|
||||
|
||||
// 将每个完整路径和对应文件放到 selectedItems
|
||||
@@ -302,31 +326,30 @@ const handleFolderSelect = async (e: Event) => {
|
||||
size: group.totalSize,
|
||||
fileCount: group.fileCount,
|
||||
files: group.files.map(file => ({ file, prefix: `${props.prefix}${folderPath}/` })),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 更新总文件数
|
||||
totalFileCount.value = currentTotal
|
||||
totalFileCount.value = currentTotal;
|
||||
|
||||
// 检查内存使用情况
|
||||
setTimeout(checkMemoryUsage, 1000)
|
||||
|
||||
setTimeout(checkMemoryUsage, 1000);
|
||||
} catch (error) {
|
||||
console.error('处理文件夹时出错:', error)
|
||||
message.error(t('Folder Processing Error'))
|
||||
console.error('处理文件夹时出错:', error);
|
||||
message.error(t('Folder Processing Error'));
|
||||
} finally {
|
||||
// 结束 loading
|
||||
isFolderLoading.value = false
|
||||
folderLoadingProgress.value = 100
|
||||
isFolderLoading.value = false;
|
||||
folderLoadingProgress.value = 100;
|
||||
|
||||
// 重置文件输入,允许重复选择同一文件夹
|
||||
input.value = ''
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.files) {
|
||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
droppedFiles.forEach(file =>
|
||||
selectedItems.value.push({
|
||||
type: 'file',
|
||||
@@ -334,83 +357,83 @@ const handleDrop = (e: DragEvent) => {
|
||||
size: file.size,
|
||||
files: [{ file, prefix: props.prefix }],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasFiles = computed(() => selectedItems.value.length > 0)
|
||||
const allFiles = computed(() => selectedItems.value.flatMap(item => item.files || []))
|
||||
const hasFiles = computed(() => selectedItems.value.length > 0);
|
||||
const allFiles = computed(() => selectedItems.value.flatMap(item => item.files || []));
|
||||
|
||||
// 内存使用级别计算
|
||||
const getMemoryUsageLevel = () => {
|
||||
const count = totalFileCount.value
|
||||
if (count < 1000) return t('Memory Low')
|
||||
if (count < MEMORY_WARNING_THRESHOLD) return t('Memory Medium')
|
||||
if (count < MAX_FILES_LIMIT) return t('Memory High')
|
||||
return t('Memory Critical')
|
||||
}
|
||||
const count = totalFileCount.value;
|
||||
if (count < 1000) return t('Memory Low');
|
||||
if (count < MEMORY_WARNING_THRESHOLD) return t('Memory Medium');
|
||||
if (count < MAX_FILES_LIMIT) return t('Memory High');
|
||||
return t('Memory Critical');
|
||||
};
|
||||
|
||||
// 格式化字节大小
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
function removeItem(index: number) {
|
||||
const item = selectedItems.value[index]
|
||||
const item = selectedItems.value[index];
|
||||
if (item.type === 'folder' && item.fileCount) {
|
||||
totalFileCount.value -= item.fileCount
|
||||
totalFileCount.value -= item.fileCount;
|
||||
} else if (item.type === 'file') {
|
||||
totalFileCount.value -= 1
|
||||
totalFileCount.value -= 1;
|
||||
}
|
||||
|
||||
selectedItems.value.splice(index, 1)
|
||||
selectedItems.value.splice(index, 1);
|
||||
|
||||
// 如果文件数量降低,重置内存警告
|
||||
if (totalFileCount.value <= MEMORY_WARNING_THRESHOLD) {
|
||||
isMemoryWarning.value = false
|
||||
isMemoryWarning.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
const filesToUpload = allFiles.value
|
||||
if (!filesToUpload.length) return
|
||||
const filesToUpload = allFiles.value;
|
||||
if (!filesToUpload.length) return;
|
||||
|
||||
isAdding.value = true
|
||||
addProgress.value = 0
|
||||
isAdding.value = true;
|
||||
addProgress.value = 0;
|
||||
|
||||
// 分组
|
||||
const uploadGroups = new Map<string, File[]>()
|
||||
const uploadGroups = new Map<string, File[]>();
|
||||
filesToUpload.forEach(fileItem => {
|
||||
const key = `${props.bucketName}:${fileItem.prefix}`
|
||||
if (!uploadGroups.has(key)) uploadGroups.set(key, [])
|
||||
uploadGroups.get(key)!.push(fileItem.file)
|
||||
})
|
||||
const key = `${props.bucketName}:${fileItem.prefix}`;
|
||||
if (!uploadGroups.has(key)) uploadGroups.set(key, []);
|
||||
uploadGroups.get(key)!.push(fileItem.file);
|
||||
});
|
||||
|
||||
// 统计总数
|
||||
const total = Array.from(uploadGroups.values()).reduce((sum, arr) => sum + arr.length, 0)
|
||||
let done = 0
|
||||
const total = Array.from(uploadGroups.values()).reduce((sum, arr) => sum + arr.length, 0);
|
||||
let done = 0;
|
||||
|
||||
// 分批异步添加,避免阻塞主线程
|
||||
for (const [key, files] of uploadGroups.entries()) {
|
||||
const [bucketName, prefix] = key.split(':')
|
||||
const [bucketName, prefix] = key.split(':');
|
||||
for (let i = 0; i < files.length; i += 50) {
|
||||
const batch = files.slice(i, i + 50)
|
||||
const batch = files.slice(i, i + 50);
|
||||
// 让出主线程,保证 UI 可响应
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
await uploadTaskManagerStore.addFiles(batch, bucketName, prefix)
|
||||
done += batch.length
|
||||
addProgress.value = Math.round((done / total) * 100)
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await uploadTaskManagerStore.addFiles(batch, bucketName, prefix);
|
||||
done += batch.length;
|
||||
addProgress.value = Math.round((done / total) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
isAdding.value = false
|
||||
addProgress.value = 100
|
||||
selectedItems.value = []
|
||||
emit('submit')
|
||||
closeModal()
|
||||
isAdding.value = false;
|
||||
addProgress.value = 100;
|
||||
selectedItems.value = [];
|
||||
emit('submit');
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
+18
-18
@@ -2,7 +2,7 @@
|
||||
* 测试环境设置
|
||||
*/
|
||||
|
||||
import { beforeEach, vi } from 'vitest'
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock console methods to avoid noise in tests
|
||||
global.console = {
|
||||
@@ -11,48 +11,48 @@ global.console = {
|
||||
log: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
}
|
||||
};
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn()
|
||||
global.fetch = vi.fn();
|
||||
|
||||
// Mock AbortSignal.timeout for older environments
|
||||
if (!AbortSignal.timeout) {
|
||||
AbortSignal.timeout = vi.fn((delay: number) => {
|
||||
const controller = new AbortController()
|
||||
setTimeout(() => controller.abort(), delay)
|
||||
return controller.signal
|
||||
})
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), delay);
|
||||
return controller.signal;
|
||||
});
|
||||
}
|
||||
|
||||
// Setup localStorage mock
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {}
|
||||
let store: Record<string, string> = {};
|
||||
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value.toString()
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key]
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {}
|
||||
store = {};
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(store).length
|
||||
return Object.keys(store).length;
|
||||
},
|
||||
key: (index: number) => Object.keys(store)[index] || null,
|
||||
}
|
||||
})()
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
})
|
||||
});
|
||||
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorageMock.clear()
|
||||
})
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* @fileoverview config-helpers.ts 模块的集成测试和边界条件测试
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
|
||||
// Mock logger
|
||||
vi.mock('~/utils/logger', () => ({
|
||||
@@ -12,7 +12,7 @@ vi.mock('~/utils/logger', () => ({
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
import {
|
||||
clearStoredHostConfig,
|
||||
@@ -23,14 +23,14 @@ import {
|
||||
getStoredHostConfig,
|
||||
saveHostConfig,
|
||||
validateConfig,
|
||||
} from '~/utils/config-helpers'
|
||||
} from '~/utils/config-helpers';
|
||||
|
||||
describe('config-helpers Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
// Setup clean environment
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(fetch).mockReset()
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetch).mockReset();
|
||||
|
||||
// Setup default window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
@@ -40,179 +40,173 @@ describe('config-helpers Integration Tests', () => {
|
||||
hostname: 'localhost',
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('完整的配置流程测试', () => {
|
||||
it('应该完成完整的配置保存和获取流程', async () => {
|
||||
const testHost = 'https://api.example.com:8080'
|
||||
const testHost = 'https://api.example.com:8080';
|
||||
|
||||
// 1. 保存配置
|
||||
const saveResult = saveHostConfig(testHost)
|
||||
expect(saveResult.config?.serverHost).toBe(testHost)
|
||||
const saveResult = saveHostConfig(testHost);
|
||||
expect(saveResult.config?.serverHost).toBe(testHost);
|
||||
|
||||
// 2. 验证保存的配置
|
||||
const storedResult = getStoredHostConfig()
|
||||
expect(storedResult.config?.serverHost).toBe(testHost)
|
||||
const storedResult = getStoredHostConfig();
|
||||
expect(storedResult.config?.serverHost).toBe(testHost);
|
||||
|
||||
// 3. 获取配置(应该优先使用 localStorage)
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
|
||||
const configResult = await getConfig()
|
||||
expect(configResult.source).toBe('localStorage')
|
||||
expect(configResult.config?.serverHost).toBe(testHost)
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
||||
const configResult = await getConfig();
|
||||
expect(configResult.source).toBe('localStorage');
|
||||
expect(configResult.config?.serverHost).toBe(testHost);
|
||||
|
||||
// 4. 清除配置
|
||||
const clearResult = clearStoredHostConfig()
|
||||
expect(clearResult).toBe(true)
|
||||
const clearResult = clearStoredHostConfig();
|
||||
expect(clearResult).toBe(true);
|
||||
|
||||
// 5. 再次获取配置(应该回退到浏览器配置)
|
||||
const fallbackResult = await getConfig()
|
||||
expect(fallbackResult.source).toBe('browser')
|
||||
expect(fallbackResult.config?.serverHost).toBe('https://localhost:3000')
|
||||
})
|
||||
const fallbackResult = await getConfig();
|
||||
expect(fallbackResult.source).toBe('browser');
|
||||
expect(fallbackResult.config?.serverHost).toBe('https://localhost:3000');
|
||||
});
|
||||
|
||||
it('应该正确处理服务器配置优先级', async () => {
|
||||
const savedHost = 'https://saved.example.com'
|
||||
const savedHost = 'https://saved.example.com';
|
||||
const serverConfig = {
|
||||
api: { baseURL: 'https://server.example.com/api' },
|
||||
s3: { endpoint: 'https://s3.server.com' },
|
||||
}
|
||||
};
|
||||
|
||||
// 保存一个配置到 localStorage
|
||||
saveHostConfig(savedHost)
|
||||
saveHostConfig(savedHost);
|
||||
|
||||
// Mock 服务器返回配置
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(serverConfig),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await getConfig()
|
||||
const result = await getConfig();
|
||||
|
||||
// 应该使用服务器配置,而不是 localStorage 配置
|
||||
expect(result.source).toBe('server')
|
||||
expect(result.config?.api.baseURL).toBe(serverConfig.api.baseURL)
|
||||
expect(result.config?.s3.endpoint).toBe(serverConfig.s3.endpoint)
|
||||
expect(result.source).toBe('server');
|
||||
expect(result.config?.api.baseURL).toBe(serverConfig.api.baseURL);
|
||||
expect(result.config?.s3.endpoint).toBe(serverConfig.s3.endpoint);
|
||||
// 但 serverHost 应该来自当前浏览器
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000')
|
||||
})
|
||||
})
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界条件和错误处理', () => {
|
||||
it('应该处理极端长度的 URL', () => {
|
||||
const longUrl = 'https://' + 'a'.repeat(2000) + '.com'
|
||||
const longUrl = 'https://' + 'a'.repeat(2000) + '.com';
|
||||
|
||||
const result = saveHostConfig(longUrl)
|
||||
const result = saveHostConfig(longUrl);
|
||||
|
||||
// 应该成功保存(URL 构造函数会验证)
|
||||
expect(result.config?.serverHost).toBe(longUrl)
|
||||
})
|
||||
expect(result.config?.serverHost).toBe(longUrl);
|
||||
});
|
||||
|
||||
it('应该处理特殊字符的 URL', () => {
|
||||
const specialUrl = 'https://测试.example.com:8080'
|
||||
const specialUrl = 'https://测试.example.com:8080';
|
||||
|
||||
const result = saveHostConfig(specialUrl)
|
||||
const result = saveHostConfig(specialUrl);
|
||||
|
||||
expect(result.config?.serverHost).toBe(specialUrl)
|
||||
})
|
||||
expect(result.config?.serverHost).toBe(specialUrl);
|
||||
});
|
||||
|
||||
it('应该处理各种无效的 URL 格式', () => {
|
||||
const invalidUrls = [
|
||||
'',
|
||||
'not-a-url',
|
||||
'https://',
|
||||
]
|
||||
const invalidUrls = ['', 'not-a-url', 'https://'];
|
||||
|
||||
invalidUrls.forEach(url => {
|
||||
const result = saveHostConfig(url)
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.error).toContain('Invalid server host format')
|
||||
})
|
||||
const result = saveHostConfig(url);
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.error).toContain('Invalid server host format');
|
||||
});
|
||||
|
||||
// 这些 URL 在技术上是有效的,但可能不实用
|
||||
const technicallyValidUrls = [
|
||||
'https://.',
|
||||
'https://...',
|
||||
'javascript:alert(1)', // 虽然不安全,但在技术上是有效的 URL
|
||||
]
|
||||
];
|
||||
|
||||
technicallyValidUrls.forEach(url => {
|
||||
const result = saveHostConfig(url)
|
||||
const result = saveHostConfig(url);
|
||||
// 这些 URL 会被 URL 构造函数接受,所以测试它们被接受
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.serverHost).toBe(url)
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.serverHost).toBe(url);
|
||||
});
|
||||
|
||||
// ftp:// 是有效的 URL 格式,但我们可能不希望支持
|
||||
// 这里单独测试以确保行为一致
|
||||
const ftpResult = saveHostConfig('ftp://invalid-protocol.com')
|
||||
const ftpResult = saveHostConfig('ftp://invalid-protocol.com');
|
||||
// ftp URL 在技术上是有效的,所以这个测试可能会通过
|
||||
// 如果需要限制协议,需要在 saveHostConfig 中添加额外验证
|
||||
expect(ftpResult.config).toBeTruthy() // ftp 是有效的 URL 协议
|
||||
})
|
||||
expect(ftpResult.config).toBeTruthy(); // ftp 是有效的 URL 协议
|
||||
});
|
||||
|
||||
it('应该处理 localStorage 异常', () => {
|
||||
// Mock localStorage 抛出异常
|
||||
const originalSetItem = localStorage.setItem
|
||||
const originalSetItem = localStorage.setItem;
|
||||
localStorage.setItem = vi.fn(() => {
|
||||
throw new Error('Storage quota exceeded')
|
||||
})
|
||||
throw new Error('Storage quota exceeded');
|
||||
});
|
||||
|
||||
const result = saveHostConfig('https://example.com')
|
||||
const result = saveHostConfig('https://example.com');
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.error).toContain('Invalid server host format')
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.error).toContain('Invalid server host format');
|
||||
|
||||
// 恢复 localStorage
|
||||
localStorage.setItem = originalSetItem
|
||||
})
|
||||
localStorage.setItem = originalSetItem;
|
||||
});
|
||||
|
||||
it('应该处理 fetch 超时', async () => {
|
||||
// Mock fetch 超时
|
||||
vi.mocked(fetch).mockImplementationOnce(() =>
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 100)
|
||||
)
|
||||
)
|
||||
vi.mocked(fetch).mockImplementationOnce(
|
||||
() => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100))
|
||||
);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.error).toContain('Failed to fetch server config')
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.error).toContain('Failed to fetch server config');
|
||||
});
|
||||
|
||||
it('应该处理损坏的 JSON 响应', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.error).toContain('Failed to fetch server config')
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.error).toContain('Failed to fetch server config');
|
||||
});
|
||||
|
||||
it('应该处理部分配置响应', async () => {
|
||||
const partialConfig = {
|
||||
api: { baseURL: 'https://custom-api.com' },
|
||||
// 缺少 s3 配置
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(partialConfig),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('server')
|
||||
expect(result.config?.api.baseURL).toBe(partialConfig.api.baseURL)
|
||||
expect(result.source).toBe('server');
|
||||
expect(result.config?.api.baseURL).toBe(partialConfig.api.baseURL);
|
||||
// 应该使用默认的 s3 配置
|
||||
expect(result.config?.s3.endpoint).toBe('https://localhost:3000')
|
||||
expect(result.config?.s3.region).toBe('us-east-1')
|
||||
})
|
||||
})
|
||||
expect(result.config?.s3.endpoint).toBe('https://localhost:3000');
|
||||
expect(result.config?.s3.region).toBe('us-east-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('并发和竞态条件测试', () => {
|
||||
it('应该处理并发的配置获取请求', async () => {
|
||||
@@ -221,57 +215,57 @@ describe('config-helpers Integration Tests', () => {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ api: { baseURL: 'test' } }),
|
||||
} as Response)
|
||||
)
|
||||
);
|
||||
|
||||
// 同时发起多个请求
|
||||
const promises = Array(5).fill(null).map(() => getConfig())
|
||||
const results = await Promise.all(promises)
|
||||
const promises = Array(5)
|
||||
.fill(null)
|
||||
.map(() => getConfig());
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// 所有请求都应该成功
|
||||
results.forEach(result => {
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.source).toBe('server')
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.source).toBe('server');
|
||||
});
|
||||
|
||||
// fetch 应该被调用 5 次
|
||||
expect(fetch).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
expect(fetch).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('应该处理快速的保存和获取操作', () => {
|
||||
const hosts = [
|
||||
'https://host1.com',
|
||||
'https://host2.com',
|
||||
'https://host3.com',
|
||||
]
|
||||
const hosts = ['https://host1.com', 'https://host2.com', 'https://host3.com'];
|
||||
|
||||
// 快速连续保存和获取
|
||||
hosts.forEach(host => {
|
||||
saveHostConfig(host)
|
||||
const result = getStoredHostConfig()
|
||||
expect(result.config?.serverHost).toBe(host)
|
||||
})
|
||||
})
|
||||
})
|
||||
saveHostConfig(host);
|
||||
const result = getStoredHostConfig();
|
||||
expect(result.config?.serverHost).toBe(host);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存泄漏和性能测试', () => {
|
||||
it('应该正确清理资源', async () => {
|
||||
// 创建大量配置对象
|
||||
const promises = Array(100).fill(null).map((_, i) => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ test: i }),
|
||||
} as Response)
|
||||
return fetchConfigFromServer()
|
||||
})
|
||||
const promises = Array(100)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ test: i }),
|
||||
} as Response);
|
||||
return fetchConfigFromServer();
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// 验证所有请求都完成
|
||||
expect(results).toHaveLength(100)
|
||||
expect(results).toHaveLength(100);
|
||||
|
||||
// 清理
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('应该处理大型配置对象', async () => {
|
||||
const largeConfig = {
|
||||
@@ -281,37 +275,37 @@ describe('config-helpers Integration Tests', () => {
|
||||
region: 'us-east-1',
|
||||
metadata: 'x'.repeat(10000), // 大量数据
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(largeConfig),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.source).toBe('server')
|
||||
})
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.source).toBe('server');
|
||||
});
|
||||
});
|
||||
|
||||
describe('跨浏览器兼容性测试', () => {
|
||||
it('应该在没有 AbortSignal.timeout 的环境中工作', async () => {
|
||||
const originalTimeout = AbortSignal.timeout
|
||||
const originalTimeout = AbortSignal.timeout;
|
||||
// @ts-ignore
|
||||
delete AbortSignal.timeout
|
||||
delete AbortSignal.timeout;
|
||||
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
// 应该不会抛出错误
|
||||
await expect(fetchConfigFromServer()).resolves.toBeTruthy()
|
||||
await expect(fetchConfigFromServer()).resolves.toBeTruthy();
|
||||
|
||||
// 恢复
|
||||
AbortSignal.timeout = originalTimeout
|
||||
})
|
||||
AbortSignal.timeout = originalTimeout;
|
||||
});
|
||||
|
||||
it('应该处理不同的 location 对象格式', () => {
|
||||
// 模拟不同浏览器的 location 对象
|
||||
@@ -319,46 +313,44 @@ describe('config-helpers Integration Tests', () => {
|
||||
{ protocol: 'https:', host: 'example.com' },
|
||||
{ protocol: 'http:', host: 'localhost:8080' },
|
||||
{ protocol: 'https:', host: 'subdomain.example.com:9000' },
|
||||
]
|
||||
];
|
||||
|
||||
locationVariants.forEach(location => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: location,
|
||||
writable: true,
|
||||
})
|
||||
});
|
||||
|
||||
const result = getCurrentBrowserConfig()
|
||||
expect(result.config?.serverHost).toBe(
|
||||
`${location.protocol.replace(':', '')}://${location.host}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
const result = getCurrentBrowserConfig();
|
||||
expect(result.config?.serverHost).toBe(`${location.protocol.replace(':', '')}://${location.host}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('调试和监控功能测试', () => {
|
||||
it('getConfigSources 应该提供完整的调试信息', async () => {
|
||||
// 设置各种配置源
|
||||
saveHostConfig('https://saved.com')
|
||||
saveHostConfig('https://saved.com');
|
||||
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ api: { baseURL: 'server-api' } }),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const sources = await getConfigSources()
|
||||
const sources = await getConfigSources();
|
||||
|
||||
// 验证所有源都有信息
|
||||
expect(sources.browser.config).toBeTruthy()
|
||||
expect(sources.localStorage.config).toBeTruthy()
|
||||
expect(sources.server.config).toBeTruthy()
|
||||
expect(sources.default.config).toBeTruthy()
|
||||
expect(sources.browser.config).toBeTruthy();
|
||||
expect(sources.localStorage.config).toBeTruthy();
|
||||
expect(sources.server.config).toBeTruthy();
|
||||
expect(sources.default.config).toBeTruthy();
|
||||
|
||||
// 验证源标识正确
|
||||
expect(sources.browser.source).toBe('browser')
|
||||
expect(sources.localStorage.source).toBe('localStorage')
|
||||
expect(sources.server.source).toBe('server')
|
||||
expect(sources.default.source).toBe('default')
|
||||
})
|
||||
expect(sources.browser.source).toBe('browser');
|
||||
expect(sources.localStorage.source).toBe('localStorage');
|
||||
expect(sources.server.source).toBe('server');
|
||||
expect(sources.default.source).toBe('default');
|
||||
});
|
||||
|
||||
it('validateConfig 应该提供详细的验证信息', () => {
|
||||
const testCases = [
|
||||
@@ -377,13 +369,13 @@ describe('config-helpers Integration Tests', () => {
|
||||
} as SiteConfig,
|
||||
expectedErrors: 2, // 缺少 s3.endpoint, s3.region
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
testCases.forEach(({ config, expectedErrors }) => {
|
||||
const result = validateConfig(config)
|
||||
expect(result.errors).toHaveLength(expectedErrors)
|
||||
expect(result.valid).toBe(expectedErrors === 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
const result = validateConfig(config);
|
||||
expect(result.errors).toHaveLength(expectedErrors);
|
||||
expect(result.valid).toBe(expectedErrors === 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* @fileoverview config-helpers.ts 模块的性能和压力测试
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
fetchConfigFromServer,
|
||||
getConfig,
|
||||
getCurrentBrowserConfig,
|
||||
saveHostConfig,
|
||||
validateConfig,
|
||||
} from '~/utils/config-helpers'
|
||||
} from '~/utils/config-helpers';
|
||||
|
||||
import {
|
||||
BrowserMock,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
measureExecutionTime,
|
||||
runBatchTest,
|
||||
TestCleaner,
|
||||
} from './test-helpers'
|
||||
} from './test-helpers';
|
||||
|
||||
// Mock logger
|
||||
vi.mock('~/utils/logger', () => ({
|
||||
@@ -27,96 +27,86 @@ vi.mock('~/utils/logger', () => ({
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('config-helpers Performance Tests', () => {
|
||||
const browserMock = new BrowserMock()
|
||||
const cleaner = new TestCleaner()
|
||||
const browserMock = new BrowserMock();
|
||||
const cleaner = new TestCleaner();
|
||||
|
||||
beforeEach(() => {
|
||||
browserMock.setLocation({ protocol: 'https:', host: 'localhost:3000' })
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(fetch).mockReset()
|
||||
})
|
||||
browserMock.setLocation({ protocol: 'https:', host: 'localhost:3000' });
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetch).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleaner.cleanup()
|
||||
})
|
||||
cleaner.cleanup();
|
||||
});
|
||||
|
||||
describe('单个函数性能测试', () => {
|
||||
it('getCurrentBrowserConfig 应该在 1ms 内完成', async () => {
|
||||
const { time } = await measureExecutionTime(() => getCurrentBrowserConfig())
|
||||
const { time } = await measureExecutionTime(() => getCurrentBrowserConfig());
|
||||
|
||||
expect(time).toBeLessThan(1)
|
||||
})
|
||||
expect(time).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('saveHostConfig 应该在 5ms 内完成', async () => {
|
||||
const { time } = await measureExecutionTime(() =>
|
||||
saveHostConfig('https://example.com:9000')
|
||||
)
|
||||
const { time } = await measureExecutionTime(() => saveHostConfig('https://example.com:9000'));
|
||||
|
||||
expect(time).toBeLessThan(5)
|
||||
})
|
||||
expect(time).toBeLessThan(5);
|
||||
});
|
||||
|
||||
it('validateConfig 应该在 1ms 内完成', async () => {
|
||||
const config = createTestConfig()
|
||||
const config = createTestConfig();
|
||||
|
||||
const { time } = await measureExecutionTime(() => validateConfig(config))
|
||||
const { time } = await measureExecutionTime(() => validateConfig(config));
|
||||
|
||||
expect(time).toBeLessThan(1)
|
||||
})
|
||||
expect(time).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('fetchConfigFromServer 应该在合理时间内完成', async () => {
|
||||
FetchMock.mockJsonResponse({ api: { baseURL: 'test' } })
|
||||
FetchMock.mockJsonResponse({ api: { baseURL: 'test' } });
|
||||
|
||||
const { time } = await measureExecutionTime(() => fetchConfigFromServer())
|
||||
const { time } = await measureExecutionTime(() => fetchConfigFromServer());
|
||||
|
||||
// 网络请求应该在 100ms 内完成(mock 环境)
|
||||
expect(time).toBeLessThan(100)
|
||||
})
|
||||
})
|
||||
expect(time).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批量操作性能测试', () => {
|
||||
it('应该处理大量的配置获取请求', async () => {
|
||||
const { results, averageTime, totalTime } = await runBatchTest(
|
||||
() => getCurrentBrowserConfig(),
|
||||
1000
|
||||
)
|
||||
const { results, averageTime, totalTime } = await runBatchTest(() => getCurrentBrowserConfig(), 1000);
|
||||
|
||||
expect(results).toHaveLength(1000)
|
||||
expect(averageTime).toBeLessThan(1) // 平均每次调用小于 1ms
|
||||
expect(totalTime).toBeLessThan(1000) // 总时间小于 1 秒
|
||||
expect(results).toHaveLength(1000);
|
||||
expect(averageTime).toBeLessThan(1); // 平均每次调用小于 1ms
|
||||
expect(totalTime).toBeLessThan(1000); // 总时间小于 1 秒
|
||||
|
||||
// 验证所有结果都是有效的
|
||||
results.forEach(result => {
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.source).toBe('browser')
|
||||
})
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.source).toBe('browser');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理大量的配置保存操作', async () => {
|
||||
const hosts = Array.from({ length: 100 }, (_, i) =>
|
||||
`https://host${i}.example.com:${9000 + i}`
|
||||
)
|
||||
const hosts = Array.from({ length: 100 }, (_, i) => `https://host${i}.example.com:${9000 + i}`);
|
||||
|
||||
const { results, averageTime } = await runBatchTest(
|
||||
() => {
|
||||
const host = hosts[Math.floor(Math.random() * hosts.length)]
|
||||
return saveHostConfig(host)
|
||||
},
|
||||
100
|
||||
)
|
||||
const { results, averageTime } = await runBatchTest(() => {
|
||||
const host = hosts[Math.floor(Math.random() * hosts.length)];
|
||||
return saveHostConfig(host);
|
||||
}, 100);
|
||||
|
||||
expect(results).toHaveLength(100)
|
||||
expect(averageTime).toBeLessThan(10) // 平均每次保存小于 10ms
|
||||
expect(results).toHaveLength(100);
|
||||
expect(averageTime).toBeLessThan(10); // 平均每次保存小于 10ms
|
||||
|
||||
// 验证所有保存操作都成功
|
||||
results.forEach(result => {
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.source).toBe('localStorage')
|
||||
})
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.source).toBe('localStorage');
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理大量的配置验证操作', async () => {
|
||||
const configs = Array.from({ length: 1000 }, (_, i) =>
|
||||
@@ -124,74 +114,71 @@ describe('config-helpers Performance Tests', () => {
|
||||
serverHost: `https://host${i}.com`,
|
||||
api: { baseURL: `https://api${i}.com` },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const { results, averageTime } = await runBatchTest(
|
||||
() => {
|
||||
const config = configs[Math.floor(Math.random() * configs.length)]
|
||||
return validateConfig(config)
|
||||
},
|
||||
1000
|
||||
)
|
||||
const { results, averageTime } = await runBatchTest(() => {
|
||||
const config = configs[Math.floor(Math.random() * configs.length)];
|
||||
return validateConfig(config);
|
||||
}, 1000);
|
||||
|
||||
expect(results).toHaveLength(1000)
|
||||
expect(averageTime).toBeLessThan(0.5) // 平均每次验证小于 0.5ms
|
||||
expect(results).toHaveLength(1000);
|
||||
expect(averageTime).toBeLessThan(0.5); // 平均每次验证小于 0.5ms
|
||||
|
||||
// 验证所有配置都通过验证
|
||||
results.forEach(result => {
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('并发操作测试', () => {
|
||||
it('应该处理并发的配置获取请求', async () => {
|
||||
FetchMock.mockJsonResponse({ api: { baseURL: 'concurrent-test' } })
|
||||
FetchMock.mockJsonResponse({ api: { baseURL: 'concurrent-test' } });
|
||||
|
||||
const concurrentRequests = 50
|
||||
const promises = Array.from({ length: concurrentRequests }, () => getConfig())
|
||||
const concurrentRequests = 50;
|
||||
const promises = Array.from({ length: concurrentRequests }, () => getConfig());
|
||||
|
||||
const start = performance.now()
|
||||
const results = await Promise.all(promises)
|
||||
const totalTime = performance.now() - start
|
||||
const start = performance.now();
|
||||
const results = await Promise.all(promises);
|
||||
const totalTime = performance.now() - start;
|
||||
|
||||
expect(results).toHaveLength(concurrentRequests)
|
||||
expect(totalTime).toBeLessThan(1000) // 所有并发请求在 1 秒内完成
|
||||
expect(results).toHaveLength(concurrentRequests);
|
||||
expect(totalTime).toBeLessThan(1000); // 所有并发请求在 1 秒内完成
|
||||
|
||||
// 验证所有结果
|
||||
results.forEach(result => {
|
||||
expect(result.config).toBeTruthy()
|
||||
})
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理并发的保存和获取操作', async () => {
|
||||
const operations = Array.from({ length: 100 }, (_, i) => {
|
||||
if (i % 2 === 0) {
|
||||
// 保存操作
|
||||
return () => saveHostConfig(`https://host${i}.com`)
|
||||
return () => saveHostConfig(`https://host${i}.com`);
|
||||
} else {
|
||||
// 获取操作
|
||||
return () => getCurrentBrowserConfig()
|
||||
return () => getCurrentBrowserConfig();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const promises = operations.map(op => op())
|
||||
const results = await Promise.all(promises)
|
||||
const promises = operations.map(op => op());
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
expect(results).toHaveLength(100)
|
||||
expect(results).toHaveLength(100);
|
||||
|
||||
// 验证结果
|
||||
results.forEach((result, index) => {
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config).toBeTruthy();
|
||||
if (index % 2 === 0) {
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.source).toBe('localStorage');
|
||||
} else {
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.source).toBe('browser');
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('内存使用测试', () => {
|
||||
it('应该不会造成内存泄漏', async () => {
|
||||
@@ -206,20 +193,20 @@ describe('config-helpers Performance Tests', () => {
|
||||
accessKeyId: `key-${i}`,
|
||||
secretAccessKey: `secret-${i}`,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// 验证配置
|
||||
validateConfig(config)
|
||||
validateConfig(config);
|
||||
|
||||
// 每 1000 次强制垃圾回收(如果可用)
|
||||
if (i % 1000 === 0 && global.gc) {
|
||||
global.gc()
|
||||
global.gc();
|
||||
}
|
||||
}
|
||||
|
||||
// 测试完成,没有抛出内存错误就算成功
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理大型配置对象', async () => {
|
||||
const largeConfig = createTestConfig({
|
||||
@@ -230,107 +217,113 @@ describe('config-helpers Performance Tests', () => {
|
||||
accessKeyId: 'x'.repeat(10000), // 10KB 的数据
|
||||
secretAccessKey: 'y'.repeat(10000), // 10KB 的数据
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const { time } = await measureExecutionTime(() => validateConfig(largeConfig))
|
||||
const { time } = await measureExecutionTime(() => validateConfig(largeConfig));
|
||||
|
||||
expect(time).toBeLessThan(10) // 即使是大型对象也应该快速处理
|
||||
})
|
||||
})
|
||||
expect(time).toBeLessThan(10); // 即使是大型对象也应该快速处理
|
||||
});
|
||||
});
|
||||
|
||||
describe('网络性能测试', () => {
|
||||
it('应该正确处理慢速网络响应', async () => {
|
||||
// Mock 慢速响应
|
||||
vi.mocked(fetch).mockImplementationOnce(() =>
|
||||
new Promise(resolve =>
|
||||
setTimeout(() => resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ api: { baseURL: 'slow-response' } }),
|
||||
} as Response), 500) // 500ms 延迟
|
||||
)
|
||||
)
|
||||
vi.mocked(fetch).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(
|
||||
resolve =>
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ api: { baseURL: 'slow-response' } }),
|
||||
} as Response),
|
||||
500
|
||||
) // 500ms 延迟
|
||||
)
|
||||
);
|
||||
|
||||
const { result, time } = await measureExecutionTime(() => fetchConfigFromServer())
|
||||
const { result, time } = await measureExecutionTime(() => fetchConfigFromServer());
|
||||
|
||||
expect(time).toBeGreaterThan(500) // 应该反映实际的网络延迟
|
||||
expect(result.config).toBeTruthy()
|
||||
})
|
||||
expect(time).toBeGreaterThan(500); // 应该反映实际的网络延迟
|
||||
expect(result.config).toBeTruthy();
|
||||
});
|
||||
|
||||
it('应该正确处理网络超时', async () => {
|
||||
// Mock 超时
|
||||
FetchMock.mockTimeout(100) // 使用较短的超时时间进行测试
|
||||
FetchMock.mockTimeout(100); // 使用较短的超时时间进行测试
|
||||
|
||||
const { result, time } = await measureExecutionTime(() => fetchConfigFromServer())
|
||||
const { result, time } = await measureExecutionTime(() => fetchConfigFromServer());
|
||||
|
||||
expect(time).toBeLessThan(1000) // 应该在 1 秒内返回
|
||||
expect(result.source).toBe('browser') // 应该回退到浏览器配置
|
||||
expect(result.error).toContain('Failed to fetch server config')
|
||||
}, 10000) // 设置测试超时为 10 秒
|
||||
})
|
||||
expect(time).toBeLessThan(1000); // 应该在 1 秒内返回
|
||||
expect(result.source).toBe('browser'); // 应该回退到浏览器配置
|
||||
expect(result.error).toContain('Failed to fetch server config');
|
||||
}, 10000); // 设置测试超时为 10 秒
|
||||
});
|
||||
|
||||
describe('压力测试', () => {
|
||||
it('应该在高负载下保持稳定', async () => {
|
||||
const highLoadOperations = []
|
||||
const highLoadOperations = [];
|
||||
|
||||
// 创建混合的高负载操作
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const operation = i % 4
|
||||
const operation = i % 4;
|
||||
switch (operation) {
|
||||
case 0:
|
||||
highLoadOperations.push(() => getCurrentBrowserConfig())
|
||||
break
|
||||
highLoadOperations.push(() => getCurrentBrowserConfig());
|
||||
break;
|
||||
case 1:
|
||||
highLoadOperations.push(() => saveHostConfig(`https://load-test-${i}.com`))
|
||||
break
|
||||
highLoadOperations.push(() => saveHostConfig(`https://load-test-${i}.com`));
|
||||
break;
|
||||
case 2:
|
||||
highLoadOperations.push(() => validateConfig(createTestConfig()))
|
||||
break
|
||||
highLoadOperations.push(() => validateConfig(createTestConfig()));
|
||||
break;
|
||||
case 3:
|
||||
FetchMock.mockJsonResponse({ test: i })
|
||||
highLoadOperations.push(() => fetchConfigFromServer())
|
||||
break
|
||||
FetchMock.mockJsonResponse({ test: i });
|
||||
highLoadOperations.push(() => fetchConfigFromServer());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const start = performance.now()
|
||||
const results = await Promise.all(highLoadOperations.map(op => op()))
|
||||
const totalTime = performance.now() - start
|
||||
const start = performance.now();
|
||||
const results = await Promise.all(highLoadOperations.map(op => op()));
|
||||
const totalTime = performance.now() - start;
|
||||
|
||||
expect(results).toHaveLength(1000)
|
||||
expect(totalTime).toBeLessThan(5000) // 5 秒内完成所有操作
|
||||
expect(results).toHaveLength(1000);
|
||||
expect(totalTime).toBeLessThan(5000); // 5 秒内完成所有操作
|
||||
|
||||
// 验证没有操作失败
|
||||
results.forEach(result => {
|
||||
expect(result).toBeTruthy()
|
||||
expect(result).toBeTruthy();
|
||||
if ('config' in result) {
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config).toBeTruthy();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理极端的并发情况', async () => {
|
||||
const extremeConcurrency = 200
|
||||
const extremeConcurrency = 200;
|
||||
|
||||
// 模拟极端并发:同时进行读写操作
|
||||
const promises = Array.from({ length: extremeConcurrency }, (_, i) => {
|
||||
if (i % 3 === 0) {
|
||||
return saveHostConfig(`https://concurrent-${i}.com`)
|
||||
return saveHostConfig(`https://concurrent-${i}.com`);
|
||||
} else if (i % 3 === 1) {
|
||||
FetchMock.mockJsonResponse({ concurrent: i })
|
||||
return fetchConfigFromServer()
|
||||
FetchMock.mockJsonResponse({ concurrent: i });
|
||||
return fetchConfigFromServer();
|
||||
} else {
|
||||
return getCurrentBrowserConfig()
|
||||
return getCurrentBrowserConfig();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
expect(results).toHaveLength(extremeConcurrency)
|
||||
expect(results).toHaveLength(extremeConcurrency);
|
||||
|
||||
// 验证所有操作都成功完成
|
||||
results.forEach(result => {
|
||||
expect(result.config).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(result.config).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+206
-210
@@ -2,8 +2,8 @@
|
||||
* @fileoverview config-helpers.ts 模块的完整测试套件
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
|
||||
// 需要 mock 的模块
|
||||
vi.mock('~/utils/logger', () => ({
|
||||
@@ -12,7 +12,7 @@ vi.mock('~/utils/logger', () => ({
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
// 导入被测试的函数
|
||||
import {
|
||||
@@ -29,10 +29,10 @@ import {
|
||||
getStoredHostConfigLegacy,
|
||||
saveHostConfig,
|
||||
validateConfig,
|
||||
} from '~/utils/config-helpers'
|
||||
} from '~/utils/config-helpers';
|
||||
|
||||
// 测试常量
|
||||
const TEST_SERVER_HOST = 'https://example.com:9000'
|
||||
const TEST_SERVER_HOST = 'https://example.com:9000';
|
||||
const TEST_CONFIG_RESPONSE = {
|
||||
api: {
|
||||
baseURL: 'https://example.com:9000/custom/api/v3',
|
||||
@@ -46,7 +46,7 @@ const TEST_CONFIG_RESPONSE = {
|
||||
session: {
|
||||
durationSeconds: 3600,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
describe('config-helpers', () => {
|
||||
// Mock window.location
|
||||
@@ -54,7 +54,7 @@ describe('config-helpers', () => {
|
||||
protocol: 'https:',
|
||||
host: 'localhost:3000',
|
||||
hostname: 'localhost',
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset to default location
|
||||
@@ -66,22 +66,22 @@ describe('config-helpers', () => {
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
});
|
||||
|
||||
// Reset fetch mock
|
||||
vi.mocked(fetch).mockReset()
|
||||
vi.mocked(fetch).mockReset();
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.clear()
|
||||
})
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createDefaultConfig', () => {
|
||||
it('应该创建正确的默认配置', () => {
|
||||
const config = createDefaultConfig(TEST_SERVER_HOST)
|
||||
const config = createDefaultConfig(TEST_SERVER_HOST);
|
||||
|
||||
expect(config).toEqual({
|
||||
serverHost: TEST_SERVER_HOST,
|
||||
@@ -94,34 +94,30 @@ describe('config-helpers', () => {
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理不同的服务器主机格式', () => {
|
||||
const testCases = [
|
||||
'http://localhost:8080',
|
||||
'https://api.example.com',
|
||||
'http://192.168.1.100:9000',
|
||||
]
|
||||
const testCases = ['http://localhost:8080', 'https://api.example.com', 'http://192.168.1.100:9000'];
|
||||
|
||||
testCases.forEach(serverHost => {
|
||||
const config = createDefaultConfig(serverHost)
|
||||
expect(config.serverHost).toBe(serverHost)
|
||||
expect(config.api.baseURL).toBe(`${serverHost}/rustfs/admin/v3`)
|
||||
expect(config.s3.endpoint).toBe(serverHost)
|
||||
})
|
||||
})
|
||||
})
|
||||
const config = createDefaultConfig(serverHost);
|
||||
expect(config.serverHost).toBe(serverHost);
|
||||
expect(config.api.baseURL).toBe(`${serverHost}/rustfs/admin/v3`);
|
||||
expect(config.s3.endpoint).toBe(serverHost);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentBrowserConfig', () => {
|
||||
it('应该基于当前浏览器位置创建配置', () => {
|
||||
const result = getCurrentBrowserConfig()
|
||||
const result = getCurrentBrowserConfig();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000')
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该处理不同的协议和端口', () => {
|
||||
// Test HTTP
|
||||
@@ -132,134 +128,134 @@ describe('config-helpers', () => {
|
||||
hostname: 'example.com',
|
||||
},
|
||||
writable: true,
|
||||
})
|
||||
});
|
||||
|
||||
const result = getCurrentBrowserConfig()
|
||||
expect(result.config?.serverHost).toBe('http://example.com:8080')
|
||||
})
|
||||
const result = getCurrentBrowserConfig();
|
||||
expect(result.config?.serverHost).toBe('http://example.com:8080');
|
||||
});
|
||||
|
||||
it('应该在非浏览器环境中返回错误', () => {
|
||||
// Mock non-browser environment
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
const result = getCurrentBrowserConfig()
|
||||
const result = getCurrentBrowserConfig();
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.error).toBe('Not in browser environment')
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.error).toBe('Not in browser environment');
|
||||
|
||||
// Restore window
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStoredHostConfig', () => {
|
||||
it('应该从 localStorage 获取保存的配置', () => {
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST)
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST);
|
||||
|
||||
const result = getStoredHostConfig()
|
||||
const result = getStoredHostConfig();
|
||||
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST)
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST);
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该在没有保存配置时返回 null', () => {
|
||||
const result = getStoredHostConfig()
|
||||
const result = getStoredHostConfig();
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.error).toBe('No saved host found')
|
||||
})
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.error).toBe('No saved host found');
|
||||
});
|
||||
|
||||
it('应该处理无效的保存配置', () => {
|
||||
localStorage.setItem('rustfs-server-host', 'invalid-url')
|
||||
localStorage.setItem('rustfs-server-host', 'invalid-url');
|
||||
|
||||
const result = getStoredHostConfig()
|
||||
const result = getStoredHostConfig();
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.error).toContain('Invalid saved host configuration')
|
||||
})
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.error).toContain('Invalid saved host configuration');
|
||||
});
|
||||
|
||||
it('应该在非浏览器环境中返回错误', () => {
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
const result = getStoredHostConfig()
|
||||
const result = getStoredHostConfig();
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.error).toBe('Not in browser environment')
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.error).toBe('Not in browser environment');
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchConfigFromServer', () => {
|
||||
it('应该成功获取并合并服务器配置', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(TEST_CONFIG_RESPONSE),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('server')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.api.baseURL).toBe(TEST_CONFIG_RESPONSE.api.baseURL)
|
||||
expect(result.config?.s3.endpoint).toBe(TEST_CONFIG_RESPONSE.s3.endpoint)
|
||||
expect(result.config?.s3.region).toBe(TEST_CONFIG_RESPONSE.s3.region)
|
||||
})
|
||||
expect(result.source).toBe('server');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.api.baseURL).toBe(TEST_CONFIG_RESPONSE.api.baseURL);
|
||||
expect(result.config?.s3.endpoint).toBe(TEST_CONFIG_RESPONSE.s3.endpoint);
|
||||
expect(result.config?.s3.region).toBe(TEST_CONFIG_RESPONSE.s3.region);
|
||||
});
|
||||
|
||||
it('应该在服务器请求失败时回退到浏览器配置', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000')
|
||||
expect(result.error).toContain('Failed to fetch server config')
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000');
|
||||
expect(result.error).toContain('Failed to fetch server config');
|
||||
});
|
||||
|
||||
it('应该处理网络错误', async () => {
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.error).toContain('Failed to fetch server config')
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.error).toContain('Failed to fetch server config');
|
||||
});
|
||||
|
||||
it('应该处理无效的服务器响应', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(null),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await fetchConfigFromServer()
|
||||
const result = await fetchConfigFromServer();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.config).toBeTruthy()
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.config).toBeTruthy();
|
||||
});
|
||||
|
||||
it('应该正确调用 fetch 并设置超时', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
await fetchConfigFromServer()
|
||||
await fetchConfigFromServer();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://localhost:3000/config.json',
|
||||
@@ -268,110 +264,110 @@ describe('config-helpers', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: expect.any(AbortSignal),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('应该优先使用服务器配置', async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(TEST_CONFIG_RESPONSE),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const result = await getConfig()
|
||||
const result = await getConfig();
|
||||
|
||||
expect(result.source).toBe('server')
|
||||
expect(result.config?.api.baseURL).toBe(TEST_CONFIG_RESPONSE.api.baseURL)
|
||||
})
|
||||
expect(result.source).toBe('server');
|
||||
expect(result.config?.api.baseURL).toBe(TEST_CONFIG_RESPONSE.api.baseURL);
|
||||
});
|
||||
|
||||
it('应该在服务器配置失败时使用 localStorage 配置', async () => {
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST)
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST);
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const result = await getConfig()
|
||||
const result = await getConfig();
|
||||
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST)
|
||||
})
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST);
|
||||
});
|
||||
|
||||
it('应该在前两个选项失败时使用浏览器配置', async () => {
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'))
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const result = await getConfig()
|
||||
const result = await getConfig();
|
||||
|
||||
expect(result.source).toBe('browser')
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000')
|
||||
})
|
||||
expect(result.source).toBe('browser');
|
||||
expect(result.config?.serverHost).toBe('https://localhost:3000');
|
||||
});
|
||||
|
||||
it('应该在所有选项失败时使用默认配置', async () => {
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
const result = await getConfig()
|
||||
const result = await getConfig();
|
||||
|
||||
expect(result.source).toBe('default')
|
||||
expect(result.config?.serverHost).toBe('http://localhost:9000')
|
||||
expect(result.source).toBe('default');
|
||||
expect(result.config?.serverHost).toBe('http://localhost:9000');
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveHostConfig', () => {
|
||||
it('应该保存有效的主机配置', () => {
|
||||
const result = saveHostConfig(TEST_SERVER_HOST)
|
||||
const result = saveHostConfig(TEST_SERVER_HOST);
|
||||
|
||||
expect(result.source).toBe('localStorage')
|
||||
expect(result.config).toBeTruthy()
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST)
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBe(TEST_SERVER_HOST)
|
||||
})
|
||||
expect(result.source).toBe('localStorage');
|
||||
expect(result.config).toBeTruthy();
|
||||
expect(result.config?.serverHost).toBe(TEST_SERVER_HOST);
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBe(TEST_SERVER_HOST);
|
||||
});
|
||||
|
||||
it('应该拒绝无效的 URL', () => {
|
||||
const result = saveHostConfig('invalid-url')
|
||||
const result = saveHostConfig('invalid-url');
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.error).toContain('Invalid server host format')
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBeNull()
|
||||
})
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.error).toContain('Invalid server host format');
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBeNull();
|
||||
});
|
||||
|
||||
it('应该在非浏览器环境中返回错误', () => {
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
const result = saveHostConfig(TEST_SERVER_HOST)
|
||||
const result = saveHostConfig(TEST_SERVER_HOST);
|
||||
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.error).toBe('Not in browser environment')
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.error).toBe('Not in browser environment');
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearStoredHostConfig', () => {
|
||||
it('应该清除保存的配置', () => {
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST)
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST);
|
||||
|
||||
const result = clearStoredHostConfig()
|
||||
const result = clearStoredHostConfig();
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBeNull()
|
||||
})
|
||||
expect(result).toBe(true);
|
||||
expect(localStorage.getItem('rustfs-server-host')).toBeNull();
|
||||
});
|
||||
|
||||
it('应该在非浏览器环境中返回 false', () => {
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
const result = clearStoredHostConfig()
|
||||
const result = clearStoredHostConfig();
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(result).toBe(false);
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfig', () => {
|
||||
it('应该验证完整的配置', () => {
|
||||
@@ -384,89 +380,89 @@ describe('config-helpers', () => {
|
||||
accessKeyId: 'key',
|
||||
secretAccessKey: 'secret',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const result = validateConfig(validConfig)
|
||||
const result = validateConfig(validConfig);
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该检测缺失的必需字段', () => {
|
||||
const invalidConfig = {
|
||||
serverHost: '',
|
||||
api: {},
|
||||
s3: {},
|
||||
} as SiteConfig
|
||||
} as SiteConfig;
|
||||
|
||||
const result = validateConfig(invalidConfig)
|
||||
const result = validateConfig(invalidConfig);
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('serverHost is required')
|
||||
expect(result.errors).toContain('api.baseURL is required')
|
||||
expect(result.errors).toContain('s3.endpoint is required')
|
||||
expect(result.errors).toContain('s3.region is required')
|
||||
})
|
||||
})
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('serverHost is required');
|
||||
expect(result.errors).toContain('api.baseURL is required');
|
||||
expect(result.errors).toContain('s3.endpoint is required');
|
||||
expect(result.errors).toContain('s3.region is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigSources', () => {
|
||||
it('应该返回所有配置源的状态', async () => {
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST)
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST);
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(TEST_CONFIG_RESPONSE),
|
||||
} as Response)
|
||||
} as Response);
|
||||
|
||||
const sources = await getConfigSources()
|
||||
const sources = await getConfigSources();
|
||||
|
||||
expect(sources.browser.source).toBe('browser')
|
||||
expect(sources.localStorage.source).toBe('localStorage')
|
||||
expect(sources.server.source).toBe('server')
|
||||
expect(sources.default.source).toBe('default')
|
||||
expect(sources.browser.source).toBe('browser');
|
||||
expect(sources.localStorage.source).toBe('localStorage');
|
||||
expect(sources.server.source).toBe('server');
|
||||
expect(sources.default.source).toBe('default');
|
||||
|
||||
expect(sources.browser.config).toBeTruthy()
|
||||
expect(sources.localStorage.config).toBeTruthy()
|
||||
expect(sources.server.config).toBeTruthy()
|
||||
expect(sources.default.config).toBeTruthy()
|
||||
})
|
||||
})
|
||||
expect(sources.browser.config).toBeTruthy();
|
||||
expect(sources.localStorage.config).toBeTruthy();
|
||||
expect(sources.server.config).toBeTruthy();
|
||||
expect(sources.default.config).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServerDefaultConfig', () => {
|
||||
it('应该返回默认的服务器配置', () => {
|
||||
const result = getServerDefaultConfig()
|
||||
const result = getServerDefaultConfig();
|
||||
|
||||
expect(result.source).toBe('default')
|
||||
expect(result.config?.serverHost).toBe('http://localhost:9000')
|
||||
expect(result.config?.api.baseURL).toBe('http://localhost:9000/rustfs/admin/v3')
|
||||
})
|
||||
})
|
||||
expect(result.source).toBe('default');
|
||||
expect(result.config?.serverHost).toBe('http://localhost:9000');
|
||||
expect(result.config?.api.baseURL).toBe('http://localhost:9000/rustfs/admin/v3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legacy Functions', () => {
|
||||
it('getStoredHostConfigLegacy 应该返回配置或 null', () => {
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST)
|
||||
localStorage.setItem('rustfs-server-host', TEST_SERVER_HOST);
|
||||
|
||||
const config = getStoredHostConfigLegacy()
|
||||
const config = getStoredHostConfigLegacy();
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config?.serverHost).toBe(TEST_SERVER_HOST)
|
||||
})
|
||||
expect(config).toBeTruthy();
|
||||
expect(config?.serverHost).toBe(TEST_SERVER_HOST);
|
||||
});
|
||||
|
||||
it('getCurrentBrowserConfigLegacy 应该返回浏览器配置', () => {
|
||||
const config = getCurrentBrowserConfigLegacy()
|
||||
const config = getCurrentBrowserConfigLegacy();
|
||||
|
||||
expect(config).toBeTruthy()
|
||||
expect(config?.serverHost).toBe('https://localhost:3000')
|
||||
})
|
||||
expect(config).toBeTruthy();
|
||||
expect(config?.serverHost).toBe('https://localhost:3000');
|
||||
});
|
||||
|
||||
it('Legacy 函数应该在失败时返回 null', () => {
|
||||
const originalWindow = global.window
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
|
||||
expect(getStoredHostConfigLegacy()).toBeNull()
|
||||
expect(getCurrentBrowserConfigLegacy()).toBeNull()
|
||||
expect(getStoredHostConfigLegacy()).toBeNull();
|
||||
expect(getCurrentBrowserConfigLegacy()).toBeNull();
|
||||
|
||||
global.window = originalWindow
|
||||
})
|
||||
})
|
||||
})
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+69
-71
@@ -2,8 +2,8 @@
|
||||
* @fileoverview 测试辅助工具和 Mock 函数
|
||||
*/
|
||||
|
||||
import { expect, vi } from 'vitest'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { expect, vi } from 'vitest';
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
|
||||
// ============================================================================
|
||||
// Mock 数据生成器
|
||||
@@ -24,7 +24,7 @@ export const createTestConfig = (overrides: Partial<SiteConfig> = {}): SiteConfi
|
||||
secretAccessKey: 'test-secret',
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建测试用的服务器配置响应
|
||||
@@ -43,7 +43,7 @@ export const createTestServerResponse = (overrides: any = {}) => ({
|
||||
durationSeconds: 3600,
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 环境 Mock 工具
|
||||
@@ -53,12 +53,12 @@ export const createTestServerResponse = (overrides: any = {}) => ({
|
||||
* Mock 浏览器环境
|
||||
*/
|
||||
export class BrowserMock {
|
||||
private originalWindow: any
|
||||
private originalLocation: any
|
||||
private originalWindow: any;
|
||||
private originalLocation: any;
|
||||
|
||||
constructor() {
|
||||
this.originalWindow = global.window
|
||||
this.originalLocation = global.window?.location
|
||||
this.originalWindow = global.window;
|
||||
this.originalLocation = global.window?.location;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,12 +73,12 @@ export class BrowserMock {
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(global.window, 'location', {
|
||||
value: { ...defaultLocation, ...location },
|
||||
writable: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,19 +86,19 @@ export class BrowserMock {
|
||||
*/
|
||||
mockNonBrowser() {
|
||||
// @ts-ignore
|
||||
delete global.window
|
||||
delete global.window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复原始环境
|
||||
*/
|
||||
restore() {
|
||||
global.window = this.originalWindow
|
||||
global.window = this.originalWindow;
|
||||
if (this.originalLocation) {
|
||||
Object.defineProperty(global.window, 'location', {
|
||||
value: this.originalLocation,
|
||||
writable: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,18 +107,18 @@ export class BrowserMock {
|
||||
* Mock localStorage
|
||||
*/
|
||||
export class LocalStorageMock {
|
||||
private store: Record<string, string> = {}
|
||||
private originalLocalStorage: any
|
||||
private store: Record<string, string> = {};
|
||||
private originalLocalStorage: any;
|
||||
|
||||
constructor() {
|
||||
this.originalLocalStorage = global.localStorage
|
||||
this.originalLocalStorage = global.localStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置初始数据
|
||||
*/
|
||||
setData(data: Record<string, string>) {
|
||||
this.store = { ...data }
|
||||
this.store = { ...data };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,13 +128,13 @@ export class LocalStorageMock {
|
||||
const mockStorage = {
|
||||
...this.createMockStorage(),
|
||||
[method]: vi.fn(() => {
|
||||
throw new Error(`localStorage ${method} error`)
|
||||
throw new Error(`localStorage ${method} error`);
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(global.window, 'localStorage', {
|
||||
value: mockStorage,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,27 +143,27 @@ export class LocalStorageMock {
|
||||
restore() {
|
||||
Object.defineProperty(global.window, 'localStorage', {
|
||||
value: this.createMockStorage(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private createMockStorage() {
|
||||
const self = this
|
||||
const self = this;
|
||||
return {
|
||||
getItem: (key: string) => self.store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
self.store[key] = value
|
||||
self.store[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete self.store[key]
|
||||
delete self.store[key];
|
||||
},
|
||||
clear: () => {
|
||||
self.store = {}
|
||||
self.store = {};
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(self.store).length
|
||||
return Object.keys(self.store).length;
|
||||
},
|
||||
key: (index: number) => Object.keys(self.store)[index] || null,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export class FetchMock {
|
||||
status: options.status ?? 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve(data),
|
||||
} as Response)
|
||||
} as Response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,25 +192,23 @@ export class FetchMock {
|
||||
status,
|
||||
statusText,
|
||||
json: () => Promise.reject(new Error('Response not ok')),
|
||||
} as Response)
|
||||
} as Response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 网络错误
|
||||
*/
|
||||
static mockNetworkError(message: string = 'Network error') {
|
||||
return vi.mocked(fetch).mockRejectedValueOnce(new Error(message))
|
||||
return vi.mocked(fetch).mockRejectedValueOnce(new Error(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 超时错误
|
||||
*/
|
||||
static mockTimeout(delay: number = 100) {
|
||||
return vi.mocked(fetch).mockImplementationOnce(
|
||||
() => new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), delay)
|
||||
)
|
||||
)
|
||||
return vi
|
||||
.mocked(fetch)
|
||||
.mockImplementationOnce(() => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), delay)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +220,7 @@ export class FetchMock {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
} as Response)
|
||||
} as Response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +249,7 @@ export const generateUrlTestCases = () => ({
|
||||
'javascript:alert(1)',
|
||||
'data:text/html,<script>alert(1)</script>',
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成各种配置测试用例
|
||||
@@ -275,7 +273,7 @@ export const generateConfigTestCases = () => ({
|
||||
s3: {},
|
||||
} as SiteConfig, // 部分字段为空
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 断言辅助函数
|
||||
@@ -285,40 +283,40 @@ export const generateConfigTestCases = () => ({
|
||||
* 验证配置对象的结构
|
||||
*/
|
||||
export const expectValidConfig = (config: SiteConfig | null) => {
|
||||
expect(config).toBeTruthy()
|
||||
expect(config?.serverHost).toBeTruthy()
|
||||
expect(config?.api?.baseURL).toBeTruthy()
|
||||
expect(config?.s3?.endpoint).toBeTruthy()
|
||||
expect(config?.s3?.region).toBeTruthy()
|
||||
}
|
||||
expect(config).toBeTruthy();
|
||||
expect(config?.serverHost).toBeTruthy();
|
||||
expect(config?.api?.baseURL).toBeTruthy();
|
||||
expect(config?.s3?.endpoint).toBeTruthy();
|
||||
expect(config?.s3?.region).toBeTruthy();
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证 ConfigResult 的结构
|
||||
*/
|
||||
export const expectValidConfigResult = (result: any, expectedSource?: string) => {
|
||||
expect(result).toHaveProperty('config')
|
||||
expect(result).toHaveProperty('source')
|
||||
expect(result).toHaveProperty('config');
|
||||
expect(result).toHaveProperty('source');
|
||||
|
||||
if (expectedSource) {
|
||||
expect(result.source).toBe(expectedSource)
|
||||
expect(result.source).toBe(expectedSource);
|
||||
}
|
||||
|
||||
if (result.config) {
|
||||
expectValidConfig(result.config)
|
||||
expectValidConfig(result.config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证错误结果
|
||||
*/
|
||||
export const expectErrorResult = (result: any, expectedSource?: string) => {
|
||||
expect(result.config).toBeNull()
|
||||
expect(result.error).toBeTruthy()
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.error).toBeTruthy();
|
||||
|
||||
if (expectedSource) {
|
||||
expect(result.source).toBe(expectedSource)
|
||||
expect(result.source).toBe(expectedSource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 性能测试工具
|
||||
@@ -328,12 +326,12 @@ export const expectErrorResult = (result: any, expectedSource?: string) => {
|
||||
* 测量函数执行时间
|
||||
*/
|
||||
export const measureExecutionTime = async <T>(fn: () => Promise<T> | T): Promise<{ result: T; time: number }> => {
|
||||
const start = performance.now()
|
||||
const result = await fn()
|
||||
const time = performance.now() - start
|
||||
const start = performance.now();
|
||||
const result = await fn();
|
||||
const time = performance.now() - start;
|
||||
|
||||
return { result, time }
|
||||
}
|
||||
return { result, time };
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量执行测试
|
||||
@@ -342,18 +340,18 @@ export const runBatchTest = async <T>(
|
||||
fn: () => Promise<T> | T,
|
||||
count: number = 100
|
||||
): Promise<{ results: T[]; averageTime: number; totalTime: number }> => {
|
||||
const start = performance.now()
|
||||
const results: T[] = []
|
||||
const start = performance.now();
|
||||
const results: T[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
results.push(await fn())
|
||||
results.push(await fn());
|
||||
}
|
||||
|
||||
const totalTime = performance.now() - start
|
||||
const averageTime = totalTime / count
|
||||
const totalTime = performance.now() - start;
|
||||
const averageTime = totalTime / count;
|
||||
|
||||
return { results, averageTime, totalTime }
|
||||
}
|
||||
return { results, averageTime, totalTime };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 清理工具
|
||||
@@ -363,13 +361,13 @@ export const runBatchTest = async <T>(
|
||||
* 测试环境清理器
|
||||
*/
|
||||
export class TestCleaner {
|
||||
private cleanupFunctions: (() => void)[] = []
|
||||
private cleanupFunctions: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* 添加清理函数
|
||||
*/
|
||||
addCleanup(fn: () => void) {
|
||||
this.cleanupFunctions.push(fn)
|
||||
this.cleanupFunctions.push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,11 +376,11 @@ export class TestCleaner {
|
||||
cleanup() {
|
||||
this.cleanupFunctions.forEach(fn => {
|
||||
try {
|
||||
fn()
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.warn('Cleanup function failed:', error)
|
||||
console.warn('Cleanup function failed:', error);
|
||||
}
|
||||
})
|
||||
this.cleanupFunctions = []
|
||||
});
|
||||
this.cleanupFunctions = [];
|
||||
}
|
||||
}
|
||||
|
||||
+130
-124
@@ -8,8 +8,8 @@
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { logger } from './logger'
|
||||
import type { SiteConfig } from '~/types/config';
|
||||
import { logger } from './logger';
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义
|
||||
@@ -18,7 +18,7 @@ import { logger } from './logger'
|
||||
/**
|
||||
* 配置来源类型
|
||||
*/
|
||||
type ConfigSource = 'browser' | 'localStorage' | 'server' | 'default'
|
||||
type ConfigSource = 'browser' | 'localStorage' | 'server' | 'default';
|
||||
|
||||
/**
|
||||
* 服务器配置响应类型(部分配置,用于合并)
|
||||
@@ -27,24 +27,24 @@ interface ServerConfigResponse {
|
||||
/** API 配置 */
|
||||
api?: {
|
||||
/** API 基础 URL */
|
||||
baseURL?: string
|
||||
}
|
||||
baseURL?: string;
|
||||
};
|
||||
/** S3 配置 */
|
||||
s3?: {
|
||||
/** S3 端点 */
|
||||
endpoint?: string
|
||||
endpoint?: string;
|
||||
/** S3 区域 */
|
||||
region?: string
|
||||
region?: string;
|
||||
/** 访问密钥 ID */
|
||||
accessKeyId?: string
|
||||
accessKeyId?: string;
|
||||
/** 秘密访问密钥 */
|
||||
secretAccessKey?: string
|
||||
}
|
||||
secretAccessKey?: string;
|
||||
};
|
||||
/** 会话配置 */
|
||||
session?: {
|
||||
/** 会话持续时间(秒) */
|
||||
durationSeconds?: number
|
||||
}
|
||||
durationSeconds?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +52,11 @@ interface ServerConfigResponse {
|
||||
*/
|
||||
interface ConfigResult {
|
||||
/** 获取到的配置,如果失败则为 null */
|
||||
config: SiteConfig | null
|
||||
config: SiteConfig | null;
|
||||
/** 配置来源 */
|
||||
source: ConfigSource
|
||||
source: ConfigSource;
|
||||
/** 错误信息(如果有) */
|
||||
error?: string
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,22 +64,22 @@ interface ConfigResult {
|
||||
*/
|
||||
interface HostInfo {
|
||||
/** 协议(http/https) */
|
||||
protocol: string
|
||||
protocol: string;
|
||||
/** 主机名和端口 */
|
||||
host: string
|
||||
host: string;
|
||||
/** 完整的服务器主机地址 */
|
||||
serverHost: string
|
||||
serverHost: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 常量定义
|
||||
// ============================================================================
|
||||
|
||||
const STORAGE_KEY = 'rustfs-server-host'
|
||||
const DEFAULT_REGION = 'us-east-1'
|
||||
const API_PATH = '/rustfs/admin/v3'
|
||||
const CONFIG_PATH = '/config.json'
|
||||
const REQUEST_TIMEOUT = 5000
|
||||
const STORAGE_KEY = 'rustfs-server-host';
|
||||
const DEFAULT_REGION = 'us-east-1';
|
||||
const API_PATH = '/rustfs/admin/v3';
|
||||
const CONFIG_PATH = '/config.json';
|
||||
const REQUEST_TIMEOUT = 5000;
|
||||
|
||||
// ============================================================================
|
||||
// 工具函数
|
||||
@@ -89,21 +89,21 @@ const REQUEST_TIMEOUT = 5000
|
||||
* 检查是否在浏览器环境
|
||||
* @returns 如果在浏览器环境中返回 true,否则返回 false
|
||||
*/
|
||||
const isBrowser = (): boolean => typeof window !== 'undefined'
|
||||
const isBrowser = (): boolean => typeof window !== 'undefined';
|
||||
|
||||
/**
|
||||
* 获取当前主机信息
|
||||
* @returns 主机信息对象,如果不在浏览器环境中则返回 null
|
||||
*/
|
||||
const getCurrentHostInfo = (): HostInfo | null => {
|
||||
if (!isBrowser()) return null
|
||||
if (!isBrowser()) return null;
|
||||
|
||||
const protocol = window.location.protocol.replace(':', '')
|
||||
const host = window.location.host
|
||||
const serverHost = `${protocol}://${host}`
|
||||
const protocol = window.location.protocol.replace(':', '');
|
||||
const host = window.location.host;
|
||||
const serverHost = `${protocol}://${host}`;
|
||||
|
||||
return { protocol, host, serverHost }
|
||||
}
|
||||
return { protocol, host, serverHost };
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证服务器配置响应
|
||||
@@ -111,8 +111,8 @@ const getCurrentHostInfo = (): HostInfo | null => {
|
||||
* @returns 如果是有效的服务器配置则返回 true
|
||||
*/
|
||||
const isValidServerConfig = (config: unknown): config is ServerConfigResponse => {
|
||||
return typeof config === 'object' && config !== null
|
||||
}
|
||||
return typeof config === 'object' && config !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
@@ -137,8 +137,8 @@ export const createDefaultConfig = (serverHost: string): SiteConfig => {
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置获取函数
|
||||
@@ -160,25 +160,25 @@ export const createDefaultConfig = (serverHost: string): SiteConfig => {
|
||||
*/
|
||||
export const getStoredHostConfig = (): ConfigResult => {
|
||||
if (!isBrowser()) {
|
||||
return { config: null, source: 'localStorage', error: 'Not in browser environment' }
|
||||
return { config: null, source: 'localStorage', error: 'Not in browser environment' };
|
||||
}
|
||||
|
||||
const savedHost = localStorage.getItem(STORAGE_KEY)
|
||||
const savedHost = localStorage.getItem(STORAGE_KEY);
|
||||
if (!savedHost) {
|
||||
return { config: null, source: 'localStorage', error: 'No saved host found' }
|
||||
return { config: null, source: 'localStorage', error: 'No saved host found' };
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(savedHost)
|
||||
const serverHost = `${url.protocol}//${url.host}`
|
||||
const config = createDefaultConfig(serverHost)
|
||||
return { config, source: 'localStorage' }
|
||||
const url = new URL(savedHost);
|
||||
const serverHost = `${url.protocol}//${url.host}`;
|
||||
const config = createDefaultConfig(serverHost);
|
||||
return { config, source: 'localStorage' };
|
||||
} catch (error) {
|
||||
const errorMessage = `Invalid saved host configuration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
logger.warn(errorMessage)
|
||||
return { config: null, source: 'localStorage', error: errorMessage }
|
||||
const errorMessage = `Invalid saved host configuration: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logger.warn(errorMessage);
|
||||
return { config: null, source: 'localStorage', error: errorMessage };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前浏览器主机配置
|
||||
@@ -192,47 +192,47 @@ export const getStoredHostConfig = (): ConfigResult => {
|
||||
* ```
|
||||
*/
|
||||
export const getCurrentBrowserConfig = (): ConfigResult => {
|
||||
const hostInfo = getCurrentHostInfo()
|
||||
const hostInfo = getCurrentHostInfo();
|
||||
if (!hostInfo) {
|
||||
return { config: null, source: 'browser', error: 'Not in browser environment' }
|
||||
return { config: null, source: 'browser', error: 'Not in browser environment' };
|
||||
}
|
||||
|
||||
const config = createDefaultConfig(hostInfo.serverHost)
|
||||
return { config, source: 'browser' }
|
||||
}
|
||||
const config = createDefaultConfig(hostInfo.serverHost);
|
||||
return { config, source: 'browser' };
|
||||
};
|
||||
|
||||
/**
|
||||
* 从服务器获取原始配置数据
|
||||
*/
|
||||
const fetchRawConfigFromServer = async (serverHost: string): Promise<ServerConfigResponse | null> => {
|
||||
const configUrl = `${serverHost}${CONFIG_PATH}`
|
||||
const configUrl = `${serverHost}${CONFIG_PATH}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(configUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(`Failed to fetch config from ${configUrl}: ${response.status} ${response.statusText}`)
|
||||
return null
|
||||
logger.warn(`Failed to fetch config from ${configUrl}: ${response.status} ${response.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const data = await response.json();
|
||||
|
||||
if (!isValidServerConfig(data)) {
|
||||
logger.warn('Invalid server config: not a valid object')
|
||||
return null
|
||||
logger.warn('Invalid server config: not a valid object');
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info(`Successfully loaded config from server: ${configUrl}`)
|
||||
return data
|
||||
logger.info(`Successfully loaded config from server: ${configUrl}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.warn(`Error fetching config from server: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
return null
|
||||
logger.warn(`Error fetching config from server: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 合并服务器配置与默认配置
|
||||
@@ -249,29 +249,35 @@ const mergeServerConfig = (baseConfig: SiteConfig, serverConfig: ServerConfigRes
|
||||
accessKeyId: serverConfig.s3?.accessKeyId || baseConfig.s3.accessKeyId,
|
||||
secretAccessKey: serverConfig.s3?.secretAccessKey || baseConfig.s3.secretAccessKey,
|
||||
},
|
||||
session: serverConfig.session ? {
|
||||
durationSeconds: serverConfig.session.durationSeconds || 0,
|
||||
} : baseConfig.session,
|
||||
}
|
||||
}
|
||||
session: serverConfig.session
|
||||
? {
|
||||
durationSeconds: serverConfig.session.durationSeconds || 0,
|
||||
}
|
||||
: baseConfig.session,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 从服务器获取配置并与默认配置合并
|
||||
*/
|
||||
export const fetchConfigFromServer = async (): Promise<ConfigResult> => {
|
||||
const browserResult = getCurrentBrowserConfig()
|
||||
const browserResult = getCurrentBrowserConfig();
|
||||
if (!browserResult.config) {
|
||||
return { config: null, source: 'server', error: browserResult.error }
|
||||
return { config: null, source: 'server', error: browserResult.error };
|
||||
}
|
||||
|
||||
const serverConfig = await fetchRawConfigFromServer(browserResult.config.serverHost)
|
||||
const serverConfig = await fetchRawConfigFromServer(browserResult.config.serverHost);
|
||||
if (!serverConfig) {
|
||||
return { config: browserResult.config, source: 'browser', error: 'Failed to fetch server config, using browser config' }
|
||||
return {
|
||||
config: browserResult.config,
|
||||
source: 'browser',
|
||||
error: 'Failed to fetch server config, using browser config',
|
||||
};
|
||||
}
|
||||
|
||||
const mergedConfig = mergeServerConfig(browserResult.config, serverConfig)
|
||||
return { config: mergedConfig, source: 'server' }
|
||||
}
|
||||
const mergedConfig = mergeServerConfig(browserResult.config, serverConfig);
|
||||
return { config: mergedConfig, source: 'server' };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 高级配置获取函数
|
||||
@@ -298,35 +304,35 @@ export const fetchConfigFromServer = async (): Promise<ConfigResult> => {
|
||||
*/
|
||||
export const getConfig = async (): Promise<ConfigResult> => {
|
||||
// 1. 尝试从服务器获取配置
|
||||
const serverResult = await fetchConfigFromServer()
|
||||
const serverResult = await fetchConfigFromServer();
|
||||
if (serverResult.config && serverResult.source === 'server') {
|
||||
return serverResult
|
||||
return serverResult;
|
||||
}
|
||||
|
||||
// 2. 尝试从 localStorage 获取配置
|
||||
const storedResult = getStoredHostConfig()
|
||||
const storedResult = getStoredHostConfig();
|
||||
if (storedResult.config) {
|
||||
return storedResult
|
||||
return storedResult;
|
||||
}
|
||||
|
||||
// 3. 使用当前浏览器配置
|
||||
const browserResult = getCurrentBrowserConfig()
|
||||
const browserResult = getCurrentBrowserConfig();
|
||||
if (browserResult.config) {
|
||||
return browserResult
|
||||
return browserResult;
|
||||
}
|
||||
|
||||
// 4. 使用默认配置
|
||||
return getServerDefaultConfig()
|
||||
}
|
||||
return getServerDefaultConfig();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取服务端默认配置
|
||||
*/
|
||||
export const getServerDefaultConfig = (): ConfigResult => {
|
||||
const defaultServerHost = 'http://localhost:9000'
|
||||
const config = createDefaultConfig(defaultServerHost)
|
||||
return { config, source: 'default' }
|
||||
}
|
||||
const defaultServerHost = 'http://localhost:9000';
|
||||
const config = createDefaultConfig(defaultServerHost);
|
||||
return { config, source: 'default' };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 兼容性函数(保持向后兼容)
|
||||
@@ -336,17 +342,17 @@ export const getServerDefaultConfig = (): ConfigResult => {
|
||||
* @deprecated 使用 getStoredHostConfig() 替代
|
||||
*/
|
||||
export const getStoredHostConfigLegacy = (): SiteConfig | null => {
|
||||
const result = getStoredHostConfig()
|
||||
return result.config
|
||||
}
|
||||
const result = getStoredHostConfig();
|
||||
return result.config;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 使用 getCurrentBrowserConfig() 替代
|
||||
*/
|
||||
export const getCurrentBrowserConfigLegacy = (): SiteConfig | null => {
|
||||
const result = getCurrentBrowserConfig()
|
||||
return result.config
|
||||
}
|
||||
const result = getCurrentBrowserConfig();
|
||||
return result.config;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置管理函数
|
||||
@@ -357,67 +363,67 @@ export const getCurrentBrowserConfigLegacy = (): SiteConfig | null => {
|
||||
*/
|
||||
export const saveHostConfig = (serverHost: string): ConfigResult => {
|
||||
if (!isBrowser()) {
|
||||
return { config: null, source: 'localStorage', error: 'Not in browser environment' }
|
||||
return { config: null, source: 'localStorage', error: 'Not in browser environment' };
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证 URL 格式
|
||||
new URL(serverHost)
|
||||
localStorage.setItem(STORAGE_KEY, serverHost)
|
||||
new URL(serverHost);
|
||||
localStorage.setItem(STORAGE_KEY, serverHost);
|
||||
|
||||
const config = createDefaultConfig(serverHost)
|
||||
logger.info(`Saved host configuration: ${serverHost}`)
|
||||
return { config, source: 'localStorage' }
|
||||
const config = createDefaultConfig(serverHost);
|
||||
logger.info(`Saved host configuration: ${serverHost}`);
|
||||
return { config, source: 'localStorage' };
|
||||
} catch (error) {
|
||||
const errorMessage = `Invalid server host format: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
logger.warn(errorMessage)
|
||||
return { config: null, source: 'localStorage', error: errorMessage }
|
||||
const errorMessage = `Invalid server host format: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
logger.warn(errorMessage);
|
||||
return { config: null, source: 'localStorage', error: errorMessage };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除保存的主机配置
|
||||
*/
|
||||
export const clearStoredHostConfig = (): boolean => {
|
||||
if (!isBrowser()) return false
|
||||
if (!isBrowser()) return false;
|
||||
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
logger.info('Cleared stored host configuration')
|
||||
return true
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
logger.info('Cleared stored host configuration');
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.warn('Failed to clear stored host configuration:', error)
|
||||
return false
|
||||
logger.warn('Failed to clear stored host configuration:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证配置是否完整
|
||||
*/
|
||||
export const validateConfig = (config: SiteConfig): { valid: boolean; errors: string[] } => {
|
||||
const errors: string[] = []
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.serverHost) {
|
||||
errors.push('serverHost is required')
|
||||
errors.push('serverHost is required');
|
||||
}
|
||||
|
||||
if (!config.api?.baseURL) {
|
||||
errors.push('api.baseURL is required')
|
||||
errors.push('api.baseURL is required');
|
||||
}
|
||||
|
||||
if (!config.s3?.endpoint) {
|
||||
errors.push('s3.endpoint is required')
|
||||
errors.push('s3.endpoint is required');
|
||||
}
|
||||
|
||||
if (!config.s3?.region) {
|
||||
errors.push('s3.region is required')
|
||||
errors.push('s3.region is required');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 调试和工具函数
|
||||
@@ -427,23 +433,23 @@ export const validateConfig = (config: SiteConfig): { valid: boolean; errors: st
|
||||
* 获取所有可用的配置源信息(用于调试)
|
||||
*/
|
||||
export const getConfigSources = async (): Promise<{
|
||||
browser: ConfigResult
|
||||
localStorage: ConfigResult
|
||||
server: ConfigResult
|
||||
default: ConfigResult
|
||||
browser: ConfigResult;
|
||||
localStorage: ConfigResult;
|
||||
server: ConfigResult;
|
||||
default: ConfigResult;
|
||||
}> => {
|
||||
const [browser, localStorage, server] = await Promise.all([
|
||||
Promise.resolve(getCurrentBrowserConfig()),
|
||||
Promise.resolve(getStoredHostConfig()),
|
||||
fetchConfigFromServer(),
|
||||
])
|
||||
]);
|
||||
|
||||
const defaultConfig = getServerDefaultConfig()
|
||||
const defaultConfig = getServerDefaultConfig();
|
||||
|
||||
return {
|
||||
browser,
|
||||
localStorage,
|
||||
server,
|
||||
default: defaultConfig,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@@ -16,4 +16,4 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user