Spaces:
Paused
Paused
| /** | |
| * K-Vault storage regression helper | |
| * | |
| * Usage example: | |
| * BASE_URL=http://localhost:8080 \ | |
| * BASIC_USER=admin BASIC_PASS=yourpass \ | |
| * SMOKE_STORAGE_TYPE=webdav \ | |
| * SMOKE_STORAGE_CONFIG_JSON='{"baseUrl":"https://dav.example.com","username":"u","password":"p"}' \ | |
| * node scripts/storage-regression.js | |
| */ | |
| const BASE_URL = String(process.env.BASE_URL || 'http://localhost:8080').replace(/\/+$/, ''); | |
| const BASIC_USER = String(process.env.BASIC_USER || ''); | |
| const BASIC_PASS = String(process.env.BASIC_PASS || ''); | |
| const SMOKE_STORAGE_TYPE = String(process.env.SMOKE_STORAGE_TYPE || '').trim().toLowerCase(); | |
| const SMOKE_STORAGE_CONFIG_JSON = String(process.env.SMOKE_STORAGE_CONFIG_JSON || '').trim(); | |
| const TEST_ONLY_TYPES = String(process.env.TEST_ONLY_TYPES || '') | |
| .split(',') | |
| .map((v) => v.trim().toLowerCase()) | |
| .filter(Boolean); | |
| const state = { | |
| cookies: new Map(), | |
| }; | |
| function setCookieFromHeaders(headers) { | |
| const cookieHeaders = typeof headers.getSetCookie === 'function' ? headers.getSetCookie() : []; | |
| for (const cookieLine of cookieHeaders) { | |
| const first = String(cookieLine || '').split(';')[0].trim(); | |
| if (!first.includes('=')) continue; | |
| const [name, ...rest] = first.split('='); | |
| state.cookies.set(name, rest.join('=')); | |
| } | |
| } | |
| function cookieHeader() { | |
| if (!state.cookies.size) return ''; | |
| return [...state.cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; '); | |
| } | |
| function buildUrl(path) { | |
| if (/^https?:\/\//i.test(path)) return path; | |
| return `${BASE_URL}${path}`; | |
| } | |
| async function request(path, { method = 'GET', headers = {}, body = null, allowError = false } = {}) { | |
| const url = buildUrl(path); | |
| const cookie = cookieHeader(); | |
| const finalHeaders = { ...headers }; | |
| if (cookie) finalHeaders.Cookie = cookie; | |
| const response = await fetch(url, { | |
| method, | |
| headers: finalHeaders, | |
| body, | |
| redirect: 'follow', | |
| }); | |
| setCookieFromHeaders(response.headers); | |
| const contentType = response.headers.get('content-type') || ''; | |
| const payload = contentType.includes('application/json') | |
| ? await response.json().catch(() => ({})) | |
| : await response.text().catch(() => ''); | |
| if (!allowError && !response.ok) { | |
| const message = typeof payload === 'string' | |
| ? payload | |
| : payload.error || payload.message || `HTTP ${response.status}`; | |
| throw new Error(`${method} ${path} failed: ${message}`); | |
| } | |
| return { response, payload }; | |
| } | |
| function logStep(title) { | |
| process.stdout.write(`\n[STEP] ${title}\n`); | |
| } | |
| function logOk(message) { | |
| process.stdout.write(` ✔ ${message}\n`); | |
| } | |
| async function ensureLoginIfNeeded() { | |
| logStep('Auth check + login'); | |
| const authCheck = await request('/api/auth/check'); | |
| const authRequired = Boolean(authCheck.payload?.authRequired); | |
| logOk(`authRequired = ${authRequired}`); | |
| if (!authRequired) { | |
| logOk('Auth disabled, skip login payload checks'); | |
| return; | |
| } | |
| if (!BASIC_USER || !BASIC_PASS) { | |
| throw new Error('Auth is required. Set BASIC_USER and BASIC_PASS for regression script.'); | |
| } | |
| const loginNew = await request('/api/auth/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: BASIC_USER, password: BASIC_PASS }), | |
| }); | |
| if (!loginNew.payload?.success) { | |
| throw new Error('Login with {username,password} did not succeed.'); | |
| } | |
| logOk('login payload {username,password} passed'); | |
| const loginCompat = await request('/api/auth/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ user: BASIC_USER, pass: BASIC_PASS }), | |
| }); | |
| if (!loginCompat.payload?.success) { | |
| throw new Error('Login with {user,pass} did not succeed.'); | |
| } | |
| logOk('login payload {user,pass} passed'); | |
| } | |
| async function checkHealthAndStatus() { | |
| logStep('Health + Status'); | |
| const health = await request('/api/health'); | |
| if (!health.payload?.ok) throw new Error('/api/health did not return ok=true'); | |
| logOk('/api/health ok'); | |
| const status = await request('/api/status'); | |
| const storageKeys = ['telegram', 'r2', 's3', 'discord', 'huggingface', 'webdav', 'github']; | |
| for (const key of storageKeys) { | |
| if (!Object.prototype.hasOwnProperty.call(status.payload || {}, key)) { | |
| throw new Error(`/api/status missing key: ${key}`); | |
| } | |
| } | |
| logOk('/api/status includes all storage keys'); | |
| } | |
| function parseSmokeConfig() { | |
| if (!SMOKE_STORAGE_CONFIG_JSON) return null; | |
| try { | |
| return JSON.parse(SMOKE_STORAGE_CONFIG_JSON); | |
| } catch (error) { | |
| throw new Error(`SMOKE_STORAGE_CONFIG_JSON is not valid JSON: ${error.message}`); | |
| } | |
| } | |
| async function storageCrudAndSelection() { | |
| logStep('Storage list / create / update / test / default'); | |
| const listed = await request('/api/storage/list'); | |
| const initialItems = listed.payload?.items || []; | |
| logOk(`initial storage configs: ${initialItems.length}`); | |
| const originalDefault = initialItems.find((item) => item.isDefault)?.id || null; | |
| let created = null; | |
| const smokeConfig = parseSmokeConfig(); | |
| if (SMOKE_STORAGE_TYPE && smokeConfig) { | |
| const createRes = await request('/api/storage', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name: `smoke-${SMOKE_STORAGE_TYPE}-${Date.now()}`, | |
| type: SMOKE_STORAGE_TYPE, | |
| enabled: true, | |
| isDefault: false, | |
| config: smokeConfig, | |
| }), | |
| }); | |
| created = createRes.payload?.item; | |
| if (!created?.id) throw new Error('Storage create succeeded without item.id'); | |
| logOk(`created storage config: ${created.id}`); | |
| const updateRes = await request(`/api/storage/${encodeURIComponent(created.id)}`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| name: `${created.name}-updated`, | |
| }), | |
| }); | |
| if (!updateRes.payload?.item?.name?.endsWith('-updated')) { | |
| throw new Error('Storage update did not apply expected name suffix.'); | |
| } | |
| logOk('update storage config passed'); | |
| const createdTest = await request(`/api/storage/${encodeURIComponent(created.id)}/test`, { | |
| method: 'POST', | |
| allowError: true, | |
| }); | |
| const testBody = createdTest.payload || {}; | |
| const testResult = testBody.result || testBody; | |
| logOk(`created storage test -> connected=${Boolean(testResult.connected)} status=${testResult.status || 'n/a'}`); | |
| await request(`/api/storage/default/${encodeURIComponent(created.id)}`, { | |
| method: 'POST', | |
| }); | |
| logOk('set created config as default passed'); | |
| } else { | |
| logOk('create/update skipped (set SMOKE_STORAGE_TYPE + SMOKE_STORAGE_CONFIG_JSON to enable)'); | |
| } | |
| return { createdId: created?.id || null, originalDefault }; | |
| } | |
| async function uploadDownloadDeleteForEnabledStorages() { | |
| logStep('Upload / Download / Delete by storage'); | |
| const listed = await request('/api/storage/list'); | |
| const allItems = (listed.payload?.items || []).filter((item) => item.enabled); | |
| const items = TEST_ONLY_TYPES.length | |
| ? allItems.filter((item) => TEST_ONLY_TYPES.includes(String(item.type || '').toLowerCase())) | |
| : allItems; | |
| if (!items.length) { | |
| logOk('no enabled storage configs found, skip file IO checks'); | |
| return; | |
| } | |
| for (const item of items) { | |
| const marker = `smoke-${item.type}-${Date.now()}-${Math.random().toString(16).slice(2)}`; | |
| const fileName = `smoke-${item.type}.txt`; | |
| const file = new File([marker], fileName, { type: 'text/plain' }); | |
| const body = new FormData(); | |
| body.append('file', file); | |
| body.append('storageId', item.id); | |
| const uploadRes = await request('/upload', { | |
| method: 'POST', | |
| body, | |
| }); | |
| const src = Array.isArray(uploadRes.payload) | |
| ? uploadRes.payload[0]?.src | |
| : uploadRes.payload?.src; | |
| if (!src) { | |
| throw new Error(`upload for storage=${item.type} did not return src`); | |
| } | |
| logOk(`${item.type}: upload passed`); | |
| const downloadRes = await request(src, { allowError: true }); | |
| if (downloadRes.response.status >= 400) { | |
| throw new Error(`${item.type}: download failed with status ${downloadRes.response.status}`); | |
| } | |
| const text = typeof downloadRes.payload === 'string' | |
| ? downloadRes.payload | |
| : ''; | |
| if (!text.includes(marker)) { | |
| throw new Error(`${item.type}: downloaded content mismatch`); | |
| } | |
| logOk(`${item.type}: download passed`); | |
| const fileId = decodeURIComponent(String(src).split('/file/')[1] || ''); | |
| if (!fileId) { | |
| throw new Error(`${item.type}: cannot parse fileId from src=${src}`); | |
| } | |
| const deleteRes = await request(`/api/manage/delete/${encodeURIComponent(fileId)}`, { | |
| method: 'GET', | |
| allowError: true, | |
| }); | |
| if (!deleteRes.payload?.success) { | |
| throw new Error(`${item.type}: delete failed: ${deleteRes.payload?.error || 'unknown error'}`); | |
| } | |
| logOk(`${item.type}: delete passed`); | |
| } | |
| } | |
| async function cleanupStorage(createdId, originalDefaultId) { | |
| logStep('Cleanup'); | |
| if (originalDefaultId) { | |
| await request(`/api/storage/default/${encodeURIComponent(originalDefaultId)}`, { | |
| method: 'POST', | |
| allowError: true, | |
| }); | |
| logOk('restored original default storage (best effort)'); | |
| } | |
| if (createdId) { | |
| await request(`/api/storage/${encodeURIComponent(createdId)}`, { | |
| method: 'DELETE', | |
| allowError: true, | |
| }); | |
| logOk('deleted created smoke storage (best effort)'); | |
| } | |
| } | |
| async function main() { | |
| process.stdout.write(`K-Vault storage regression start\nBASE_URL=${BASE_URL}\n`); | |
| await checkHealthAndStatus(); | |
| await ensureLoginIfNeeded(); | |
| const { createdId, originalDefault } = await storageCrudAndSelection(); | |
| await uploadDownloadDeleteForEnabledStorages(); | |
| await cleanupStorage(createdId, originalDefault); | |
| process.stdout.write('\nAll regression checks completed.\n'); | |
| } | |
| main().catch((error) => { | |
| process.stderr.write(`\nRegression failed: ${error.message}\n`); | |
| process.exit(1); | |
| }); | |