implement admin db testing
This commit is contained in:
261
chat/server.js
261
chat/server.js
@@ -20,7 +20,7 @@ const blogSystem = require('./blog-system');
|
||||
const versionManager = require('./src/utils/versionManager');
|
||||
const { DATA_ROOT, STATE_DIR, DB_PATH, KEY_FILE } = require('./src/database/config');
|
||||
const { initDatabase, getDatabase, closeDatabase } = require('./src/database/connection');
|
||||
const { initEncryption, encrypt } = require('./src/utils/encryption');
|
||||
const { initEncryption, encrypt, decrypt, isEncryptionInitialized } = require('./src/utils/encryption');
|
||||
|
||||
let sharp = null;
|
||||
try {
|
||||
@@ -17279,6 +17279,259 @@ async function handleAdminOllamaTest(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdminSystemTests(req, res) {
|
||||
const adminSession = requireAdminAuth(req, res);
|
||||
if (!adminSession) return;
|
||||
|
||||
const startedAt = Date.now();
|
||||
const results = [];
|
||||
|
||||
const pushResult = (name, status, details, meta, durationMs) => {
|
||||
results.push({
|
||||
name,
|
||||
status,
|
||||
details: details || '',
|
||||
meta: meta || null,
|
||||
durationMs: durationMs || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const runCheck = async (name, fn) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const outcome = await fn();
|
||||
const status = outcome?.status || 'passed';
|
||||
pushResult(name, status, outcome?.details || '', outcome?.meta || null, Date.now() - start);
|
||||
} catch (error) {
|
||||
pushResult(
|
||||
name,
|
||||
'failed',
|
||||
error?.message || String(error),
|
||||
{ error: String(error) },
|
||||
Date.now() - start
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await runCheck('database.enabled', async () => {
|
||||
const db = getDatabase();
|
||||
if (!databaseEnabled || !db || !db.open) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: `Database disabled (${databaseFallbackReason || 'unknown'})`,
|
||||
meta: {
|
||||
dbPath: DB_PATH,
|
||||
enabled: databaseEnabled,
|
||||
fallbackReason: databaseFallbackReason || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'passed',
|
||||
details: `Database enabled (${DB_PATH})`,
|
||||
meta: {
|
||||
dbPath: DB_PATH,
|
||||
sqlcipher: DATABASE_USE_SQLCIPHER,
|
||||
walMode: DATABASE_WAL_MODE,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await runCheck('database.schema', async () => {
|
||||
const db = getDatabase();
|
||||
if (!databaseEnabled || !db || !db.open) {
|
||||
return { status: 'skipped', details: 'Database not enabled' };
|
||||
}
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
||||
const tableNames = new Set(tables.map((t) => t.name));
|
||||
const missing = [];
|
||||
|
||||
if (!tableNames.has('users')) missing.push('users');
|
||||
if (!tableNames.has('affiliate_accounts')) missing.push('affiliate_accounts');
|
||||
if (!tableNames.has('sessions')) missing.push('sessions');
|
||||
|
||||
const userColumns = db.prepare("PRAGMA table_info('users')").all();
|
||||
const userColumnNames = new Set(userColumns.map((c) => c.name));
|
||||
if (!userColumnNames.has('data')) missing.push('users.data');
|
||||
|
||||
if (missing.length) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: `Missing schema: ${missing.join(', ')}`,
|
||||
meta: { tables: Array.from(tableNames).sort().slice(0, 40) },
|
||||
};
|
||||
}
|
||||
return { status: 'passed', details: 'Schema looks healthy' };
|
||||
});
|
||||
|
||||
await runCheck('encryption.roundtrip', async () => {
|
||||
if (!databaseEnabled) {
|
||||
return { status: 'skipped', details: 'Database disabled; encryption not initialized' };
|
||||
}
|
||||
if (!isEncryptionInitialized()) {
|
||||
return { status: 'failed', details: 'Encryption not initialized' };
|
||||
}
|
||||
const sample = `self-test-${randomUUID()}`;
|
||||
const encrypted = encrypt(sample);
|
||||
const decrypted = decrypt(encrypted);
|
||||
if (decrypted !== sample) {
|
||||
return { status: 'failed', details: 'Encryption roundtrip mismatch' };
|
||||
}
|
||||
return { status: 'passed', details: 'Encryption roundtrip ok' };
|
||||
});
|
||||
|
||||
await runCheck('account.create', async () => {
|
||||
const testEmail = `admin-selftest-${Date.now()}@example.com`;
|
||||
const testPassword = `SelfTest1!XyZ${String(Date.now()).slice(-4)}`;
|
||||
const passwordValidation = validatePassword(testPassword);
|
||||
if (!passwordValidation.valid) {
|
||||
return { status: 'failed', details: `Generated password failed validation: ${passwordValidation.errors.join(', ')}` };
|
||||
}
|
||||
|
||||
let testUser = null;
|
||||
try {
|
||||
testUser = await createUser(testEmail, testPassword);
|
||||
const found = findUserByEmail(testEmail);
|
||||
if (!found || found.id !== testUser.id) {
|
||||
return { status: 'failed', details: 'User not found after creation' };
|
||||
}
|
||||
|
||||
const loginUser = await verifyUserPassword(testEmail, testPassword);
|
||||
if (!loginUser || loginUser.id !== testUser.id) {
|
||||
return { status: 'failed', details: 'Password verification failed' };
|
||||
}
|
||||
|
||||
if (databaseEnabled) {
|
||||
const db = getDatabase();
|
||||
if (!db) return { status: 'failed', details: 'Database not initialized during account test' };
|
||||
const row = db.prepare('SELECT email, email_encrypted, password_hash, data FROM users WHERE id = ?').get(testUser.id);
|
||||
if (!row) {
|
||||
return { status: 'failed', details: 'User row missing from database' };
|
||||
}
|
||||
if (row.email !== testEmail) {
|
||||
return { status: 'failed', details: 'Database email mismatch after creation' };
|
||||
}
|
||||
if (!row.password_hash) {
|
||||
return { status: 'failed', details: 'Database password hash missing' };
|
||||
}
|
||||
if (!row.email_encrypted) {
|
||||
return { status: 'failed', details: 'Encrypted email missing in database' };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'passed', details: 'Account created and verified' };
|
||||
} finally {
|
||||
if (testUser) {
|
||||
const index = usersDb.findIndex((u) => u && u.id === testUser.id);
|
||||
if (index >= 0) {
|
||||
usersDb.splice(index, 1);
|
||||
await persistUsersDb();
|
||||
}
|
||||
if (databaseEnabled) {
|
||||
const db = getDatabase();
|
||||
if (db) {
|
||||
try {
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(testUser.id);
|
||||
} catch (_) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await runCheck('payments.topups.config', async () => {
|
||||
if (!DODO_ENABLED) {
|
||||
return { status: 'skipped', details: 'Dodo Payments disabled' };
|
||||
}
|
||||
const pack = resolveTopupPack('topup_1', 'usd');
|
||||
const baseAmount = getTopupPrice(pack.tier, pack.currency);
|
||||
if (!pack.productId || !pack.tokens || !baseAmount) {
|
||||
return {
|
||||
status: 'failed',
|
||||
details: 'Top-up configuration missing (productId, tokens, or price)',
|
||||
meta: { pack, baseAmount },
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'passed',
|
||||
details: `Top-up configured (${pack.tokens} tokens for ${pack.currency.toUpperCase()} ${baseAmount})`,
|
||||
meta: { productId: pack.productId },
|
||||
};
|
||||
});
|
||||
|
||||
await runCheck('payments.topups.checkout', async () => {
|
||||
if (!DODO_ENABLED) {
|
||||
return { status: 'skipped', details: 'Dodo Payments disabled' };
|
||||
}
|
||||
const { pack, baseAmount } = await fetchTopupProduct('topup_1', 'usd');
|
||||
const unitAmount = applyTopupDiscount(baseAmount, 0);
|
||||
const orderId = `admin_self_test_${randomUUID()}`;
|
||||
const isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || ''));
|
||||
const adminEmail = isValidEmail(ADMIN_USER) ? ADMIN_USER : 'admin@example.com';
|
||||
const checkoutBody = {
|
||||
product_cart: [{
|
||||
product_id: pack.productId,
|
||||
quantity: 1,
|
||||
amount: unitAmount,
|
||||
}],
|
||||
customer: {
|
||||
email: adminEmail,
|
||||
name: ADMIN_USER || 'Admin',
|
||||
},
|
||||
metadata: {
|
||||
type: 'admin_self_test',
|
||||
orderId,
|
||||
admin: 'true',
|
||||
tokens: String(pack.tokens),
|
||||
tier: String(pack.tier),
|
||||
currency: String(pack.currency),
|
||||
amount: String(unitAmount),
|
||||
},
|
||||
settings: {
|
||||
redirect_immediately: false,
|
||||
},
|
||||
return_url: `${resolveBaseUrl(req)}/test-checkout`,
|
||||
};
|
||||
|
||||
const checkoutSession = await dodoRequest('/checkouts', {
|
||||
method: 'POST',
|
||||
body: checkoutBody,
|
||||
});
|
||||
|
||||
const sessionId = checkoutSession?.session_id || checkoutSession?.id || '';
|
||||
if (!sessionId || !checkoutSession?.checkout_url) {
|
||||
return { status: 'failed', details: 'Dodo checkout did not return a checkout URL' };
|
||||
}
|
||||
return {
|
||||
status: 'passed',
|
||||
details: 'Dodo checkout created successfully',
|
||||
meta: { sessionId },
|
||||
};
|
||||
});
|
||||
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const summary = results.reduce(
|
||||
(acc, r) => {
|
||||
acc.total += 1;
|
||||
if (r.status === 'passed') acc.passed += 1;
|
||||
else if (r.status === 'failed') acc.failed += 1;
|
||||
else acc.skipped += 1;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, passed: 0, failed: 0, skipped: 0 }
|
||||
);
|
||||
const ok = summary.failed === 0;
|
||||
|
||||
sendJson(res, 200, {
|
||||
ok,
|
||||
startedAt: new Date(startedAt).toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
summary,
|
||||
results,
|
||||
});
|
||||
}
|
||||
|
||||
// Get detailed resource usage breakdown by session for admin panel
|
||||
async function handleAdminResources(req, res) {
|
||||
const adminSession = requireAdminAuth(req, res);
|
||||
@@ -19551,6 +19804,7 @@ async function routeInternal(req, res, url, pathname) {
|
||||
if (req.method === 'GET' && pathname === '/api/admin/resources') return handleAdminResources(req, res);
|
||||
if (req.method === 'GET' && pathname === '/api/admin/external-testing-status') return handleAdminExternalTestingStatus(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/admin/external-testing-self-test') return handleAdminExternalTestingSelfTest(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/admin/system-tests') return handleAdminSystemTests(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/upgrade-popup-tracking') return handleUpgradePopupTracking(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/admin/cancel-messages') return handleAdminCancelMessages(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/admin/ollama-test') return handleAdminOllamaTest(req, res);
|
||||
@@ -19748,6 +20002,11 @@ async function routeInternal(req, res, url, pathname) {
|
||||
if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
|
||||
return serveFile(res, safeStaticPath('admin-resources.html'), 'text/html');
|
||||
}
|
||||
if (pathname === '/admin/system-tests') {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
|
||||
return serveFile(res, safeStaticPath('admin-system-tests.html'), 'text/html');
|
||||
}
|
||||
if (pathname === '/admin/external-testing') {
|
||||
const session = getAdminSession(req);
|
||||
if (!session) return serveFile(res, safeStaticPath('admin-login.html'), 'text/html');
|
||||
|
||||
Reference in New Issue
Block a user