🐛 fix: 修复 GitHub Actions 工作流 YAML 语法错误

- 修复 dependencies.yml 中多行字符串缩进不一致的问题
- 将 commit-message 改为单行字符串格式
- 修复 reviewers 字段的空值语法
- 确保所有 YAML 文件符合严格的语法规范
- 通过 prettier 格式化所有工作流文件
This commit is contained in:
overtrue
2025-10-07 23:19:01 +08:00
parent e52686435e
commit f05935114a
12 changed files with 939 additions and 953 deletions
+3 -9
View File
@@ -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
+12 -20
View File
@@ -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:
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
+155 -163
View File
@@ -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);
});
});
});
});
+150 -157
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, '.'),
},
},
})
});