const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const root = path.resolve(__dirname, '..', '..'); const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8'); let behavior; const harness = () => { if (!behavior) { const run = spawnSync('php', [path.join(__dirname, 'pending-upload-harness.php')], { encoding: 'utf8' }); assert.equal(run.status, 0, `${run.stderr}${run.stdout}`); assert.equal(run.stderr, ''); behavior = JSON.parse(run.stdout); } return behavior; }; test('upload marks pending and returns attachment id', () => { const controller = read('src/Admin/UserController.php'); assert.match(controller, /PendingUploads::mark\(\(int\) \$attachment_id\)/); assert.match(controller, /'id'\s*=> \(int\) \$attachment_id/); }); test('pending lifecycle: discard route, owner check, claim, cron cleanup', () => { const svc = read('src/Attachment/PendingUploads.php'); assert.match(svc, /'\/upload\/pending\/\(\?P\\d\+\)'/); assert.match(svc, /'methods'\s*=> 'DELETE'/); assert.match(svc, /'permission_callback'/); assert.match(svc, /post_author === \$user_id/); assert.match(svc, /wp_schedule_event\(time\(\) \+ HOUR_IN_SECONDS, 'hourly', self::CRON_HOOK\)/); assert.match(svc, /\$wpdb->prepare\(/); assert.match(svc, /add_action\('comment_post', \[\$this, 'claim_comment'\]/); assert.match(read('functions.php'), /PendingUploads::get_instance\(\)->init\(\)/); }); test('submit paths claim referenced uploads', () => { const rest = read('src/Ticket/TicketRest.php'); const admin = read('src/Ticket/TicketRestAdmin.php'); const report = read('src/Admin/UserControllerReportOperations.php'); assert.equal((rest.match(/PendingUploads::claim_urls\(\$attachment/g) || []).length, 2); assert.equal((admin.match(/PendingUploads::claim_urls\(\$attachment/g) || []).length, 2); assert.match(report, /PendingUploads::claim_text\(\$reason, \$reporter_id\)/); }); test('front-end remove buttons discard the pending upload', () => { const core = read('assets/js/ticket-center-core.js'); const report = read('assets/js/report.js'); assert.match(core, /'\/upload\/pending\/' \+ id/); assert.match(core, /data-aksrt-ticket-image-id/); assert.match(report, /'\/upload\/pending\/'/); }); test('front submission uploads are marked and saved references are claimed without close-time deletion', () => { const upload = read('src/Core/FrontSubmit.php'); const media = read('src/Core/FrontSubmitMedia.php'); const editor = read('assets/js/front-submit-editor.js'); assert.match(upload, /PendingUploads::mark\(\$attachment_id\)/); assert.match(media, /PendingUploads::claim_ids\(array_keys\(\$desired\), \$user_id\)/); assert.match(media, /PendingUploads::claim_urls\(\$download_urls, \$user_id, null\)/); assert.match(media, /PendingUploads::claim_text\(\(string\) \$post->post_content, \$user_id, null\)/); assert.doesNotMatch(editor, /\/upload\/pending\//); }); test('cleanup is scheduled once hourly and discard requires a logged-in user', () => { const state = harness().registration; assert.ok(state.hooks.includes('aksrt_pending_upload_cleanup')); assert.ok(state.hooks.includes('init')); assert.equal(state.schedule_count, 1); assert.equal(state.recurrence, 'hourly'); assert.equal(state.namespace, 'aksrt/v1'); assert.equal(state.method, 'DELETE'); assert.equal(state.member_allowed, true); assert.equal(state.guest_allowed, false); }); test('ID claims handle more than 20 attachments but only clear the owner’s pending marker', () => { assert.deepEqual(harness().claim_ids, { remaining: [26], marked_recently: true, invalid_marked: false }); }); test('URL and text claims retain their default bound while saved content may claim all references', () => { assert.deepEqual(harness().claim_urls, { default_remaining: [21, 22, 23, 24, 25], unlimited_remaining: [] }); assert.deepEqual(harness().claim_text, { default_remaining: [21, 22, 23, 24, 25, 26, 27], unlimited_remaining: [26, 27] }); }); test('discard refuses foreign, claimed and parented media and permanently deletes only an owned pending upload', () => { assert.deepEqual(harness().discard, { denied: [false, false, false, false], success: true, repeat_status: 404, deleted: [[1, true]] }); }); test('cleanup deletes only expired unattached pending uploads and uses prepared bounded SQL', () => { const state = harness().cleanup; assert.deepEqual(state.deleted, [[1, true]]); assert.deepEqual(state.remaining, [2, 3, 4]); assert.match(state.query.sql, /p\.post_type = 'attachment' AND p\.post_parent = 0/); assert.match(state.query.sql, /m\.meta_key = %s/); assert.match(state.query.sql, /LIMIT %d/); assert.equal(state.query.params[0], '_aksrt_pending_upload'); assert.equal(state.query.params[2], 100); assert.ok(Math.abs(state.query.params[1] - (Math.floor(Date.now() / 1000) - 86400)) < 10); }); test('cleanup rechecks selected candidates after claim, attach, refresh or removal', () => { assert.deepEqual(harness().stale_selection.deleted, [[5, true]]); }); test('cleanup processes a bounded batch and retries unsuccessful deletions later', () => { assert.deepEqual(harness().batch, { first: 100, total: 105, remaining: 0 }); assert.deepEqual(harness().retry, { retained: true, deleted_after_retry: true }); });