Mark new regular and chunked uploads as pending and claim persisted article references, including partial saves and large reference sets. Protect associated media from cleanup and add upload lifecycle regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
5.3 KiB
JavaScript
111 lines
5.3 KiB
JavaScript
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<id>\\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 });
|
||
});
|