import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { Algorithm as Argon2Algorithm, hashRawSync as argon2HashRawSync } from '@node-rs/argon2'; import { createUserAuth, clearUserLogoutCookies, userLoginCookies } from './user-auth.mjs'; test('ensureAllUserDataSpaces provisions every active non-admin user', async () => { const calls = []; const pool = { async query(sql) { if (sql.includes("WHERE role = 'user' AND status = 'active'")) { return [[ { id: 'user-1', workspace_root: '/tmp/user-1' }, { id: 'user-2', workspace_root: '/tmp/user-2' }, ]]; } return [[]]; }, }; const auth = createUserAuth(pool, { persistSessions: false, provisionUserDataSpace: async (input) => calls.push(input), }); const result = await auth.ensureAllUserDataSpaces(); assert.equal(result.provisioned, 2); assert.deepEqual(result.errors, []); assert.deepEqual(calls, [ { userId: 'user-1', workspaceRoot: '/tmp/user-1' }, { userId: 'user-2', workspaceRoot: '/tmp/user-2' }, ]); }); function hashPasswordPbkdf2(password, salt) { return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex'); } function hashPasswordArgon2id(password, salt) { return argon2HashRawSync(password, { salt: Buffer.from(salt, 'hex'), parallelism: 1, outputLen: 32, memoryCost: 64 * 1024, timeCost: 3, algorithm: Argon2Algorithm.Argon2id, }).toString('hex'); } function createAuthPool(userRow) { const sessions = []; return { async query(sql, params = []) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.username = ?')) { return userRow ? [[userRow]] : [[]]; } if (sql.includes('UPDATE h5_users') && sql.includes('password_algorithm')) { userRow.salt = params[0]; userRow.password_hash = params[1]; userRow.password_algorithm = params[2]; return [[]]; } if (sql.includes('INSERT INTO h5_login_sessions')) { sessions.push({ token_hash: params[2], user_id: params[1], expires_at: params[3], revoked_at: null, }); return [[]]; } if (sql.includes('FROM h5_login_sessions s')) { const row = sessions.find((item) => item.token_hash === params[0] && !item.revoked_at); if (!row) return [[]]; return [[ { user_id: row.user_id, expires_at: row.expires_at, role: userRow.role, status: userRow.status, }, ]]; } if (sql.includes('UPDATE h5_login_sessions SET expires_at')) { const row = sessions.find((item) => item.token_hash === params[1]); if (row) row.expires_at = params[0]; return [[]]; } if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) { for (const row of sessions) { if (row.user_id === params[1]) row.revoked_at = params[0]; } return [[]]; } if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } return [[]]; }, }; } function createAdminLayoutPool(adminRow) { return { async query(sql, params = []) { if (sql.includes('SELECT id FROM h5_users WHERE username = ? LIMIT 1')) { return adminRow ? [[{ id: adminRow.id }]] : [[]]; } if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[adminRow]]; } if (sql.includes('FROM h5_assets a') && sql.includes('JOIN h5_space_categories')) { return [[]]; } if (sql.includes('UPDATE h5_users SET workspace_root = ?')) { adminRow.workspace_root = params[0]; return [[]]; } if (sql.includes('DELETE FROM h5_user_path_grants')) return [[]]; if (sql.includes('INSERT INTO h5_user_path_grants')) return [[]]; if (sql.includes('UPDATE h5_user_wallets SET balance_cents')) return [[]]; if (sql.includes('FROM h5_login_sessions')) return [[]]; return [[]]; }, }; } function createRoleSkillSeedPool(adminRow, { existingRoleSkills = {} } = {}) { const roleSkills = { ...existingRoleSkills }; const roleSkillSeedSql = []; const basePool = createAdminLayoutPool(adminRow); return { roleSkills, roleSkillSeedSql, async query(sql, params = []) { if ( sql.includes('INSERT INTO h5_user_skill_grants') && sql.includes("VALUES ('role', 'user'") ) { roleSkillSeedSql.push(sql); const [skillName, enabled] = params; if (skillName in roleSkills) { if (!sql.includes('ON DUPLICATE KEY UPDATE skill_name = skill_name')) { roleSkills[skillName] = enabled; } } else { roleSkills[skillName] = enabled; } return [[]]; } return basePool.query(sql, params); }, }; } function createAgentPolicyPool(userRow) { return { async query(sql, params = []) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } if (sql.includes('FROM h5_assets a') && sql.includes('JOIN h5_space_categories')) { return [[]]; } if (sql.includes('FROM h5_user_capability_grants')) return [[]]; if (sql.includes('FROM h5_user_policy_grants')) return [[]]; if (sql.includes('FROM h5_user_skill_grants')) return [[]]; if (sql.includes('UPDATE h5_users SET workspace_root = ?')) { userRow.workspace_root = params[0]; return [[]]; } if (sql.includes('DELETE FROM h5_user_path_grants')) return [[]]; if (sql.includes('INSERT INTO h5_user_path_grants')) return [[]]; return [[]]; }, }; } test('login rate limits repeated failures', async () => { const auth = createUserAuth(createAuthPool(null), { loginMaxFailures: 2, loginFailureWindowMs: 60_000, persistSessions: false, }); assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false); assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false); const blocked = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' }); assert.equal(blocked.ok, false); assert.ok(blocked.retryAfterMs > 0); }); test('verify rejects disabled users after login', async () => { const salt = '00112233445566778899aabbccddeeff'; const userRow = { id: 'user-1', username: 'john', slug: 'john', email: 'john@example.com', display_name: 'John', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/john', salt, password_hash: hashPasswordArgon2id('secret', salt), password_algorithm: 'argon2id', balance_cents: 100, tokens_used: 0, }; const auth = createUserAuth(createAuthPool(userRow), { persistSessions: true }); const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' }); assert.equal(login.ok, true); userRow.status = 'disabled'; assert.equal(await auth.verify(login.token), null); }); test('resetPassword updates hash when username and email match', async () => { const salt = '11112222333344445555666677778888'; const userRow = { id: 'user-1', username: 'john', email: 'john@example.com', status: 'active', salt, password_hash: hashPasswordPbkdf2('old-secret', salt), password_algorithm: 'pbkdf2-sha512', }; const pool = { async query(sql, params = []) { if (sql.includes('SELECT id, email, status FROM h5_users WHERE username = ?')) { return userRow.username === params[0] ? [[userRow]] : [[]]; } if (sql.includes('UPDATE h5_users SET salt = ?')) { userRow.salt = params[0]; userRow.password_hash = params[1]; userRow.password_algorithm = params[2]; return [[]]; } if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) { return [[]]; } return [[]]; }, }; const auth = createUserAuth(pool, { persistSessions: false }); const ok = await auth.resetPassword({ username: 'john', email: 'john@example.com', password: 'new-secret', }); assert.equal(ok.ok, true); assert.equal(userRow.password_algorithm, 'argon2id'); assert.equal(userRow.password_hash, hashPasswordArgon2id('new-secret', userRow.salt)); const bad = await auth.resetPassword({ username: 'john', email: 'wrong@example.com', password: 'another', }); assert.equal(bad.ok, false); }); test('login migrates pbkdf2 passwords to argon2id after successful verification', async () => { const salt = '9999aaaabbbbccccddddeeeeffff0000'; const userRow = { id: 'user-1', username: 'john', slug: 'john', email: 'john@example.com', display_name: 'John', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/john', salt, password_hash: hashPasswordPbkdf2('secret', salt), password_algorithm: 'pbkdf2-sha512', balance_cents: 100, tokens_used: 0, }; const auth = createUserAuth(createAuthPool(userRow), { persistSessions: false }); const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' }); assert.equal(login.ok, true); assert.equal(userRow.password_algorithm, 'argon2id'); assert.equal(userRow.password_hash, hashPasswordArgon2id('secret', userRow.salt)); }); test('admin users get a real MindSpace publish layout', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-layout-')); const adminRow = { id: 'admin-1', username: 'admin', slug: 'admin', email: 'admin@example.com', display_name: '管理员', role: 'admin', status: 'active', plan_type: 'free', workspace_root: path.join(root, 'legacy-admin-root'), balance_cents: 0, tokens_used: 0, }; const auth = createUserAuth(createAdminLayoutPool(adminRow), { h5Root: root, env: { H5_PUBLIC_BASE_URL: 'https://example.com' }, persistSessions: false, }); const layout = await auth.getUserPublishLayout(adminRow.id); assert.ok(layout, 'admin should receive a publish layout'); assert.equal(layout.slug, adminRow.id); assert.equal(layout.username, 'admin'); assert.equal(layout.publishDir, path.join(root, 'MindSpace', adminRow.id)); assert.equal(layout.publicUrl, `https://example.com/MindSpace/${adminRow.id}/`); const workingDir = await auth.resolveWorkingDir(adminRow.id); assert.equal(workingDir, layout.publishDir); assert.equal(adminRow.workspace_root, layout.publishDir); }); test('ensureAdminUser repairs existing admin workspace without password env', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-repair-')); const adminRow = { id: 'admin-2', username: 'admin', slug: 'admin', email: 'admin@example.com', display_name: '管理员', role: 'admin', status: 'active', plan_type: 'free', workspace_root: path.join(root, 'legacy-admin-root'), balance_cents: 0, tokens_used: 0, }; const auth = createUserAuth(createAdminLayoutPool(adminRow), { h5Root: root, env: { H5_PUBLIC_BASE_URL: 'https://example.com' }, persistSessions: false, }); await auth.ensureAdminUser(); assert.equal(adminRow.workspace_root, path.join(root, 'MindSpace', adminRow.id)); }); test('ensureAdminUser seedRoleSkillDefaults preserves admin-configured role skill grants', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-role-skill-seed-')); const adminRow = { id: 'admin-3', username: 'admin', slug: 'admin', email: 'admin@example.com', display_name: '管理员', role: 'admin', status: 'active', plan_type: 'free', workspace_root: path.join(root, 'MindSpace', 'admin-3'), balance_cents: 0, tokens_used: 0, }; const pool = createRoleSkillSeedPool(adminRow, { existingRoleSkills: { 'static-page-publish': 1 }, }); const auth = createUserAuth(pool, { h5Root: root, env: { H5_PUBLIC_BASE_URL: 'https://example.com' }, persistSessions: false, }); await auth.ensureAdminUser(); assert.equal(pool.roleSkills['static-page-publish'], 1); assert.ok(pool.roleSkillSeedSql.length > 0); assert.match(pool.roleSkillSeedSql[0], /ON DUPLICATE KEY UPDATE skill_name = skill_name/); assert.doesNotMatch(pool.roleSkillSeedSql.join('\n'), /ON DUPLICATE KEY UPDATE enabled = VALUES\(enabled\)/); }); test('agent session policy preserves sandbox root and exposes workspace ref metadata', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-agent-policy-')); const userRow = { id: 'user-1', username: 'john', slug: 'john', email: 'john@example.com', display_name: 'John', role: 'user', status: 'active', plan_type: 'free', workspace_root: path.join(root, 'legacy-root'), quota_bytes: 1024, used_bytes: 0, reserved_bytes: 0, balance_cents: 100, tokens_used: 0, }; const auth = createUserAuth(createAgentPolicyPool(userRow), { h5Root: root, persistSessions: false, env: { GOOSED_SANDBOX_PUBLISH_ROOT: '/srv/goosed-mindspace', GOOSED_MCP_NODE_PATH: '/usr/local/bin/node', GOOSED_MCP_SERVER_PATH: '/opt/portal/mindspace-sandbox-mcp.mjs', GOOSED_MCP_CONTAINERIZED: '1', MINDSPACE_USERDATA_BACKEND: 'postgres', MINDSPACE_USERDATA_PG_URL: 'postgresql://mindspace:secret@127.0.0.1:5433/mindspace_userdata_prod', MINDSPACE_USERDATA_AUTO_PROVISION: '1', }, }); const policy = await auth.getAgentSessionPolicy(userRow.id); const sandboxFs = policy.extensionOverrides.find((item) => item.name === 'sandbox-fs'); assert.ok(sandboxFs); assert.equal(sandboxFs.envs.SANDBOX_ROOT, path.resolve('/srv/goosed-mindspace/user-1')); assert.equal(sandboxFs.envs.MINDSPACE_WORKSPACE_ROOT, path.resolve(root, 'MindSpace', 'user-1')); assert.equal(sandboxFs.envs.MINDSPACE_WORKSPACE_REF, 'mindspace://users/user-1/workspace'); assert.equal(new URL(sandboxFs.envs.MINDSPACE_USERDATA_PG_URL).hostname, 'host.docker.internal'); assert.equal(sandboxFs.envs.MINDSPACE_USERDATA_BACKEND, 'postgres'); assert.equal(sandboxFs.envs.MINDSPACE_USERDATA_AUTO_PROVISION, '1'); }); test('admin capabilities include granted platform skills', async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-admin-skills-')); await fs.mkdir(path.join(root, 'skills', 'product-campaign-page'), { recursive: true }); await fs.writeFile( path.join(root, 'skills', 'product-campaign-page', 'SKILL.md'), '---\nname: product-campaign-page\ndescription: 商品宣传 / 活动页技能\n---\n', ); const adminRow = { id: 'admin-3', username: 'admin', slug: 'admin', email: 'admin@example.com', display_name: '管理员', role: 'admin', status: 'active', plan_type: 'free', workspace_root: '/tmp/admin', balance_cents: 0, tokens_used: 0, }; const auth = createUserAuth(createAdminLayoutPool(adminRow), { h5Root: root, persistSessions: false, }); const resolved = await auth.resolveUserCapabilities(adminRow); assert.equal(resolved.unrestricted, true); assert.ok(resolved.grantedSkills.includes('product-campaign-page')); }); test('recharge sends notifier payload after successful self recharge', async () => { const userRow = { id: 'user-1', username: 'john', slug: 'john', email: 'john@example.com', display_name: 'John', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/john', balance_cents: 1200, tokens_used: 0, spent_cents: 0, }; const queries = []; const notifierCalls = []; const pool = { async query(sql) { queries.push(sql); if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } throw new Error(`unexpected pool query: ${sql}`); }, async getConnection() { return { async beginTransaction() {}, async commit() {}, async rollback() {}, release() {}, async query(sql) { queries.push(sql); if (sql.includes('INSERT INTO h5_user_wallets')) return [{ affectedRows: 1 }, []]; if (sql.includes('INSERT INTO h5_billing_ledger')) return [{ affectedRows: 1 }, []]; if (sql.includes('INSERT INTO h5_user_notifications')) return [{ affectedRows: 1 }, []]; throw new Error(`unexpected connection query: ${sql}`); }, }; }, }; const auth = createUserAuth(pool, { persistSessions: false, onRechargeNotification: async (payload) => { notifierCalls.push(payload); }, }); const result = await auth.recharge('user-1', 2500, null, '用户自助充值', { paymentOrderId: 'order-1', }); assert.equal(result.ok, true); assert.equal(notifierCalls.length, 1); assert.equal(notifierCalls[0].notificationType, 'self_recharge'); assert.equal(notifierCalls[0].title, '充值成功'); assert.equal(notifierCalls[0].amountCents, 2500); assert.equal(notifierCalls[0].paymentOrderId, 'order-1'); assert.ok(queries.some((sql) => sql.includes('INSERT INTO h5_user_notifications'))); }); test('purchaseSpaceQuota deducts balance and increases quota', async () => { const userRow = { id: 'user-2', username: 'alice', slug: 'alice', email: 'alice@example.com', display_name: 'Alice', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/alice', balance_cents: 5000, tokens_used: 0, spent_cents: 0, quota_bytes: 5 * 1024 * 1024, used_bytes: 1024, reserved_bytes: 2048, }; const spaceRow = { id: 'space-1', quota_bytes: 5 * 1024 * 1024, used_bytes: 1024, reserved_bytes: 2048, }; const pool = { async query(sql) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) { return [[spaceRow]]; } throw new Error(`unexpected pool query: ${sql}`); }, async getConnection() { return { async beginTransaction() {}, async commit() {}, async rollback() {}, release() {}, async query(sql, params) { if (sql.includes('SELECT id, quota_bytes, used_bytes, reserved_bytes')) { return [[spaceRow]]; } if (sql.includes('SELECT balance_cents')) { return [[{ balance_cents: userRow.balance_cents }]]; } if (sql.includes('UPDATE h5_user_wallets')) { userRow.balance_cents -= params[0]; return [{ affectedRows: 1 }, []]; } if (sql.includes('UPDATE h5_user_spaces')) { spaceRow.quota_bytes += params[0]; userRow.quota_bytes = spaceRow.quota_bytes; return [{ affectedRows: 1 }, []]; } if (sql.includes('INSERT INTO h5_billing_ledger')) return [{ affectedRows: 1 }, []]; if (sql.includes('INSERT INTO h5_user_notifications')) return [{ affectedRows: 1 }, []]; throw new Error(`unexpected connection query: ${sql}`); }, }; }, }; const auth = createUserAuth(pool, { persistSessions: false }); const result = await auth.purchaseSpaceQuota('user-2', 10); assert.equal(result.ok, true); assert.equal(result.balanceCents, 3000); assert.equal(result.quota.quotaBytes, 15 * 1024 * 1024); assert.equal(result.quota.availableBytes, 15 * 1024 * 1024 - 1024 - 2048); }); test('billSessionUsage auto gifts low-balance bonus once for eligible new users', async () => { const userRow = { id: 'user-3', username: 'bonus_user', slug: 'bonus_user', email: 'bonus@example.com', display_name: 'Bonus User', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/bonus-user', balance_cents: 150, tokens_used: 0, spent_cents: 0, }; const stateBySession = new Map(); let walletBalance = 150; let tokensUsed = 0; let lowBalanceGiftEligible = 1; let lowBalanceGiftGrantedAt = null; let ledgerAdjustCount = 0; let notificationTypes = []; const pool = { async query(sql) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[{ ...userRow, balance_cents: walletBalance, tokens_used: tokensUsed }]]; } throw new Error(`unexpected pool query: ${sql}`); }, async getConnection() { return { async beginTransaction() {}, async commit() {}, async rollback() {}, release() {}, async query(sql, params = []) { if (sql.includes('SELECT cost_cents FROM h5_usage_records WHERE request_id = ? LIMIT 1')) return [[]]; if (sql.includes('INSERT INTO h5_session_billing_state') && sql.includes('agent_session_id = agent_session_id')) { return [{ affectedRows: 1 }, []]; } if (sql.includes('FROM h5_session_billing_state') && sql.includes('FOR UPDATE')) { const row = stateBySession.get(params[0]); return [row ? [row] : []]; } if (sql.includes('INSERT INTO h5_session_billing_state') && sql.includes('ON DUPLICATE KEY UPDATE')) { stateBySession.set(params[0], { last_accumulated_cost: params[2], last_input_tokens: params[3], last_output_tokens: params[4], }); return [{ affectedRows: 1 }, []]; } if (sql.includes('SELECT w.balance_cents, w.tokens_used, u.status')) { return [[{ balance_cents: walletBalance, tokens_used: tokensUsed, status: userRow.status }]]; } if (sql.includes('UPDATE h5_user_wallets') && sql.includes('tokens_used = tokens_used + ?')) { walletBalance = params[0]; tokensUsed += params[1]; return [{ affectedRows: 1 }, []]; } if (sql.includes('INSERT INTO h5_usage_records')) return [{ affectedRows: 1 }, []]; if (sql.includes("INSERT INTO h5_billing_ledger") && sql.includes("'deduct'")) return [{ affectedRows: 1 }, []]; if (sql.includes('SELECT low_balance_gift_eligible, low_balance_gift_granted_at')) { return [[{ low_balance_gift_eligible: lowBalanceGiftEligible, low_balance_gift_granted_at: lowBalanceGiftGrantedAt, }]]; } if (sql.includes('UPDATE h5_user_wallets') && sql.includes('SET balance_cents = ?, updated_at = ?')) { walletBalance = params[0]; return [{ affectedRows: 1 }, []]; } if (sql.includes('UPDATE h5_users') && sql.includes('low_balance_gift_eligible = 0')) { lowBalanceGiftEligible = 0; lowBalanceGiftGrantedAt = params[0]; userRow.status = 'active'; return [{ affectedRows: 1 }, []]; } if (sql.includes("INSERT INTO h5_billing_ledger") && sql.includes("'adjust'")) { ledgerAdjustCount += 1; return [{ affectedRows: 1 }, []]; } if (sql.includes('INSERT INTO h5_user_notifications')) { notificationTypes.push('low_balance_gift'); return [{ affectedRows: 1 }, []]; } if (sql.includes('UPDATE h5_users SET status = \'suspended\'')) { userRow.status = 'suspended'; return [{ affectedRows: 1 }, []]; } throw new Error(`unexpected connection query: ${sql}`); }, }; }, }; const auth = createUserAuth(pool, { persistSessions: false }); const first = await auth.billSessionUsage( userRow.id, 'session-bonus-1', { accumulatedOutputTokens: 10_000 }, 'req-bonus-1', ); assert.equal(first.ok, true); assert.equal(first.costCents, 60); assert.equal(first.balanceCents, 1090); assert.equal(ledgerAdjustCount, 1); assert.deepEqual(notificationTypes, ['low_balance_gift']); const second = await auth.billSessionUsage( userRow.id, 'session-bonus-2', { accumulatedOutputTokens: 10_000 }, 'req-bonus-2', ); assert.equal(second.ok, true); assert.equal(second.costCents, 60); assert.equal(second.balanceCents, 1030); assert.equal(ledgerAdjustCount, 1); assert.deepEqual(notificationTypes, ['low_balance_gift']); }); test('updateUser rejects quota smaller than occupied bytes', async () => { const userRow = { id: 'user-3', username: 'bob', slug: 'bob', email: 'bob@example.com', display_name: 'Bob', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/bob', balance_cents: 1000, tokens_used: 0, spent_cents: 0, quota_bytes: 5 * 1024 * 1024, used_bytes: 4 * 1024 * 1024, reserved_bytes: 2 * 1024 * 1024, }; const pool = { async query(sql) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) { return [[{ id: 'space-2', quota_bytes: userRow.quota_bytes, used_bytes: userRow.used_bytes, reserved_bytes: userRow.reserved_bytes, }]]; } throw new Error(`unexpected pool query: ${sql}`); }, }; const auth = createUserAuth(pool, { persistSessions: false }); const result = await auth.updateUser('user-3', { spaceQuotaBytes: 5 * 1024 * 1024, }); assert.equal(result.ok, false); assert.match(result.message, /空间不能小于已使用容量/); }); test('updateUser persists a larger space quota', async () => { const userRow = { id: 'user-4', username: 'carol', slug: 'carol', email: 'carol@example.com', display_name: 'Carol', role: 'user', status: 'active', plan_type: 'free', workspace_root: '/tmp/carol', balance_cents: 1000, tokens_used: 0, spent_cents: 0, quota_bytes: 5 * 1024 * 1024, used_bytes: 1 * 1024 * 1024, reserved_bytes: 0, }; const pool = { async query(sql, params = []) { if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) { return [[userRow]]; } if (sql.includes('FROM h5_user_spaces') && sql.includes('WHERE user_id = ?')) { return [[{ id: 'space-4', quota_bytes: userRow.quota_bytes, used_bytes: userRow.used_bytes, reserved_bytes: userRow.reserved_bytes, }]]; } if (sql.includes('UPDATE h5_user_spaces')) { userRow.quota_bytes = params[0]; return [{ affectedRows: 1 }, []]; } throw new Error(`unexpected pool query: ${sql}`); }, }; const auth = createUserAuth(pool, { persistSessions: false }); const result = await auth.updateUser('user-4', { spaceQuotaBytes: 20 * 1024 * 1024, }); assert.equal(result.ok, true); assert.equal(result.user.spaceQuotaBytes, 20 * 1024 * 1024); assert.equal(result.user.spaceAvailableBytes, 19 * 1024 * 1024); }); test('upsertWechatAgentRoute uses atomic insert-on-duplicate semantics', async () => { const calls = []; const pool = { async query(sql, params = []) { calls.push([sql, params]); if (sql.includes('INSERT INTO h5_wechat_agent_routes')) return [[]]; if (sql.includes('FROM h5_wechat_agent_routes')) { return [[{ id: 'route-1', user_id: 'user-1', agent_session_id: 'session-2', status: 'active', created_at: 1, updated_at: 2 }]]; } return [[]]; }, }; const auth = createUserAuth(pool, { persistSessions: false }); const routeId = await auth.upsertWechatAgentRoute({ userId: 'user-1', appId: 'wx123', openid: 'openid-1', agentSessionId: 'session-2', }); assert.equal(routeId, 'route-1'); assert.match(calls[0][0], /ON DUPLICATE KEY UPDATE/); assert.equal(calls.some(([sql]) => /UPDATE h5_wechat_agent_routes/.test(sql)), false); }); test('clearUserLogoutCookies clears shared-domain and host-only cookies', () => { const cookies = clearUserLogoutCookies(false, '.tkmind.cn'); assert.equal(cookies.length, 2); assert.match(cookies[0], /tkmind_user_session=/); assert.match(cookies[0], /Domain=\.tkmind\.cn/); assert.match(cookies[0], /Max-Age=0/); assert.match(cookies[1], /Max-Age=0/); assert.doesNotMatch(cookies[1], /Domain=/); }); test('userLoginCookies and clearUserLogoutCookies are symmetric for shared domain', () => { const login = userLoginCookies('token-abc', true, '.tkmind.cn'); const logout = clearUserLogoutCookies(true, '.tkmind.cn'); assert.equal(login.length, 2); assert.equal(logout.length, 2); assert.match(login[1], /Max-Age=0/); assert.match(logout[1], /Max-Age=0/); });