Files
wordpress-theme-aksrt/tests/widget-system/pending-upload-contract.test.cjs
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

111 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-jio-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('jio_pending_upload_cleanup'));
assert.ok(state.hooks.includes('init'));
assert.equal(state.schedule_count, 1);
assert.equal(state.recurrence, 'hourly');
assert.equal(state.namespace, 'jio/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], '_jio_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 });
});