Files
memind/user-auth.test.mjs
T
john 7f8d692d16 fix(agent): recover stale runs and improve new-user OA delivery
Fix DEV logout cookie clearing, materialize selected MindSpace OA assets before agent runs, and recover zombie runs from synced workspace pages. Add client run wait timeout, harness retry limits, page-edit asset forwarding, and logout/john2 scenario tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 20:05:52 +08:00

750 lines
25 KiB
JavaScript

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';
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 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('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' },
});
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');
});
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/);
});