'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const harness = path.join(__dirname, 'report-submit-harness.php'); let cachedResults; function results() { if (cachedResults) return cachedResults; const run = spawnSync(process.env.PHP_BINARY || 'php', [harness], { cwd: root, encoding: 'utf8', }); assert.ifError(run.error); assert.equal(run.status, 0, `PHP report harness failed:\n${run.stderr}\n${run.stdout}`); cachedResults = JSON.parse(run.stdout); return cachedResults; } test('stores readable preset category separately from details and evidence', () => { const result = results().preset_with_evidence; const reason = '补充详情 https://example.test/uploads/proof.png'; assert.equal(result.response.success, true); assert.equal(result.rows.length, 1); assert.equal(result.rows[0].category, '垃圾信息'); assert.equal(result.rows[0].reason, reason); assert.doesNotMatch(result.rows[0].reason, /^垃圾信息/); assert.deepEqual(result.claims, [[reason, 42]]); const hook = result.actions[0][1]; assert.equal(hook.category, '垃圾信息'); assert.equal(hook.reason, reason); }); test('accepts preset-only reports and includes their category in admin notifications', () => { const result = results().preset_label_only; assert.equal(result.response.success, true); assert.equal(result.rows[0].category, '人身攻击'); assert.equal(result.rows[0].reason, ''); assert.equal(result.messages.length, 1); assert.match(result.messages[0].content, /人身攻击/); }); test('accepts the reserved other category only when details are present', () => { const result = results().other_with_details; assert.equal(result.response.success, true); assert.equal(result.rows[0].category, '其他原因'); assert.equal(result.rows[0].reason, '请核查相关资料'); }); test('preserves long configured category labels without truncating them', () => { const result = results().long_preset_label; const label = '非常长的预置举报分类名称超过数据库原先四十字符限制并且应完整保留在工单及后台列表当中'; assert.ok(label.length > 40); assert.equal(result.response.success, true); assert.equal(result.rows[0].category, label); }); test('uses sanitized category and reason consistently in storage and the submit hook', () => { const result = results().sanitized_snapshot; assert.equal(result.response.success, true); assert.equal(result.rows[0].category, '垃圾信息'); assert.equal(result.rows[0].reason, '补充详情'); assert.deepEqual(result.actions[0][1], { reporter_id: 42, target_type: 'post', target_id: 12, reason: '补充详情', category: '垃圾信息', }); }); test('rejects blank, unknown, and hash-like categories before inserting', () => { for (const name of ['blank_category', 'unknown_category', 'hash_category']) { const result = results()[name]; assert.equal(result.response.success, false, name); assert.equal(result.insert_calls.length, 0, name); assert.equal(result.rows.length, 0, name); } }); test('requires details for other, while authentication and target validation still apply', () => { const missingOtherDetails = results().missing_other_details; assert.equal(missingOtherDetails.response.success, false); assert.equal(missingOtherDetails.insert_calls.length, 0); const unauthenticated = results().unauthenticated; assert.equal(unauthenticated.response.success, false); assert.equal(unauthenticated.status, 401); assert.equal(unauthenticated.insert_calls.length, 0); const invalidTarget = results().invalid_target; assert.equal(invalidTarget.response.success, false); assert.equal(invalidTarget.insert_calls.length, 0); }); test('prevents duplicate pending reports without claiming evidence again', () => { const result = results().duplicate; assert.equal(result.response.success, false); assert.equal(result.status, 409); assert.equal(result.rows.length, 1); assert.equal(result.insert_calls.length, 0); assert.deepEqual(result.claims, []); }); test('does not notify, fire hooks, or claim evidence after an insert failure with stale insert_id', () => { const result = results().insert_failure; assert.equal(result.response.success, false); assert.equal(result.insert_calls.length, 1); assert.equal(result.rows.length, 0); assert.deepEqual(result.messages, []); assert.deepEqual(result.actions, []); assert.deepEqual(result.claims, []); }); test('rejects a blank category at the storage service boundary', () => { const result = results().service_blank_category; assert.equal(result.id, 0); assert.equal(result.rows.length, 0); assert.equal(result.insert_calls.length, 0); }); test('preserves readable labels through the admin category filter, list, and detail', () => { const result = results().admin_category; assert.equal(result.submit_success, true); assert.equal(result.list_items.length, 1); assert.equal(result.list_items[0].category, '涉嫌侵权'); assert.ok(result.list_options.some((option) => option.label === '涉嫌侵权' && option.value === '涉嫌侵权')); assert.equal(result.detail_report.category, '涉嫌侵权'); }); test('stores report categories as text and uses schema version 3.3.0', () => { const migrator = fs.readFileSync(path.join(root, 'src/Core/DBMigrator.php'), 'utf8'); const tableDefinitions = fs.readFileSync(path.join(root, 'src/Core/DBMigratorPart2.php'), 'utf8'); const reportTable = tableDefinitions.match(/\$sql_report\s*=\s*"([\s\S]*?)";/); assert.match(migrator, /const SCHEMA_VERSION\s*=\s*'3\.3\.0';/); assert.ok(reportTable, 'report table definition should exist'); assert.match(reportTable[1], /\bcategory\s+TEXT\s+NOT NULL\b/); }); test('preserves submitted category snapshots after preset settings change or disappear', () => { const result = results().category_snapshot; const expected = ['提交时的分类', '待删除的分类']; assert.equal(result.submit_success, true); assert.deepEqual([...result.changed_list_categories].sort(), [...expected].sort()); assert.deepEqual([...result.changed_list_options].sort(), [...expected].sort()); assert.deepEqual([...result.removed_list_categories].sort(), [...expected].sort()); assert.deepEqual(result.details, expected); }); test('preserves literal zero text and exposes preset names without slugs', () => { const data = results(); const result = data.falsey_text; assert.equal(result.response.success, true); assert.equal(result.rows[0].category, '0'); assert.equal(result.rows[0].reason, '0'); assert.deepEqual(data.runtime_reasons, [{ name: '垃圾信息' }, { name: '人身攻击' }]); }); test('passes category and details to restriction actions without rewriting report content', () => { const data = results(); const preset = data.process_preset_only; assert.equal(preset.success, true); assert.equal(preset.restriction.type, 'warn'); assert.equal(preset.restriction.reason, '人身攻击'); assert.equal(preset.report_category, '人身攻击'); assert.equal(preset.report_reason, ''); const other = data.process_other_with_details; assert.equal(other.success, true); assert.equal(other.restriction.type, 'warn'); assert.equal(other.restriction.reason, '其他原因\n\n自定义说明不能丢'); assert.equal(other.report_category, '其他原因'); assert.equal(other.report_reason, '自定义说明不能丢'); assert.doesNotMatch(other.report_reason, /^其他原因/); });