fix(users): restore WeChat template notification flow
This commit is contained in:
1 parent
30ceab4615
commit
7fe26d3fff
18 files changed
+1051
-763
No files matched your search
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
|
||||
test('WeChat business event mappings and hook registration', () => {
|
||||
const harness = path.join(__dirname, 'wechat-events-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /WeChat events harness passed/);
|
||||
});
|
||||
|
||||
test('business actions are emitted by the owning workflows', () => {
|
||||
const cases = [
|
||||
['src/Identity/ApplicationService.php', 'jio_auth_application_submitted'],
|
||||
['src/User/AuthApply.php', 'jio_auth_application_reviewed'],
|
||||
['inc/modules/user-parts/account-ajax.php', 'jio_user_email_changed'],
|
||||
['inc/modules/user-notifications.php', 'jio_wechat_comment_postauthor'],
|
||||
['inc/modules/user-notifications.php', 'jio_wechat_comment_parent'],
|
||||
];
|
||||
const adapter = fs.readFileSync(path.join(root, 'src/Payment/WeChatNotifyEvents.php'), 'utf8');
|
||||
for (const [file, event] of cases) {
|
||||
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
||||
assert.ok(source.includes(`do_action('${event}'`), `${file} does not emit ${event}`);
|
||||
assert.ok(adapter.includes(`add_action('${event}'`), `adapter does not register ${event}`);
|
||||
}
|
||||
const restReports = fs.readFileSync(path.join(root, 'src/Admin/UserControllerReportOperations.php'), 'utf8');
|
||||
assert.match(restReports, /\$status !== \(string\) \(\$row\['status'\] \?\? ''\)[\s\S]*?do_action\('jio_report_status_changed'/);
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* Exercise the event adapter with a recording sender and minimal WordPress hooks.
|
||||
*/
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
$GLOBALS['wechat_event_hooks'] = [];
|
||||
|
||||
class WP_User {
|
||||
public function __construct(public int $ID, public string $display_name) {}
|
||||
}
|
||||
|
||||
class WP_Comment {
|
||||
public function __construct(
|
||||
public int $comment_ID,
|
||||
public int $comment_post_ID,
|
||||
public string $comment_date,
|
||||
public string $comment_content
|
||||
) {}
|
||||
}
|
||||
|
||||
function add_action(string $name, callable $callback, int $priority = 10, int $accepted_args = 1): void {
|
||||
$GLOBALS['wechat_event_hooks'][$name][] = [$callback, $accepted_args];
|
||||
}
|
||||
|
||||
function do_action(string $name, ...$args): void {
|
||||
foreach ($GLOBALS['wechat_event_hooks'][$name] ?? [] as [$callback, $accepted_args]) {
|
||||
$callback(...array_slice($args, 0, $accepted_args));
|
||||
}
|
||||
}
|
||||
|
||||
function __($message, $domain = ''): string { return (string) $message; }
|
||||
function current_time(string $format): string { return '2026-11-10 10:00:00'; }
|
||||
function home_url(string $path = '/'): string { return 'https://site.test' . $path; }
|
||||
function admin_url(string $path): string { return 'https://site.test/wp-admin/' . $path; }
|
||||
function jio_get_user_center_url(string $tab): string { return 'https://site.test/user/' . $tab . '/'; }
|
||||
function jio_get_user_center_tab_url(string $tab): string { return 'https://site.test/user/' . $tab . '/'; }
|
||||
function jio_get_messages_page_url(string $view): string { return 'https://site.test/messages/' . $view . '/'; }
|
||||
function get_users(array $args): array { return [2, 3]; }
|
||||
function get_userdata(int $id) { return new WP_User($id, 'User ' . $id); }
|
||||
function get_the_title(int $id): string { return 'Product ' . $id; }
|
||||
function get_comment_author(WP_Comment $comment): string { return 'Commenter ' . $comment->comment_ID; }
|
||||
function get_comment_link(WP_Comment $comment): string { return 'https://site.test/post/#comment-' . $comment->comment_ID; }
|
||||
$GLOBALS['wpdb'] = new class {
|
||||
public function prepare(string $query, int $id): string { return str_replace('%d', (string) $id, $query); }
|
||||
public function get_var(string $query): string { return str_contains($query, 'wp_revenue') ? 'O-3' : 'O-4'; }
|
||||
};
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/AssetType.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/AssetAmount.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/LedgerCommand.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/LedgerResult.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Payment/Withdrawal.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Payment/WeChatNotifyEvents.php';
|
||||
}
|
||||
|
||||
namespace Jio\Core {
|
||||
final class DBMigrator {
|
||||
public static function table(string $name): string { return 'wp_' . $name; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Jio\Payment {
|
||||
final class RecordingWechatSender {
|
||||
use WeChatNotifyEvents;
|
||||
|
||||
public array $messages = [];
|
||||
|
||||
public function init(): void {
|
||||
$this->register_event_hooks();
|
||||
}
|
||||
|
||||
public function send_event(int $user_id, string $type, array $values, string $url = ''): bool {
|
||||
$this->messages[] = compact('user_id', 'type', 'values', 'url');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
$check = static function (bool $condition, string $message): void {
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, $message . "\n");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
$sender = new \Jio\Payment\RecordingWechatSender();
|
||||
$sender->init();
|
||||
$sender->init();
|
||||
$check(count($GLOBALS['wechat_event_hooks']['jio_order_paid'] ?? []) === 1, 'order hook registered twice');
|
||||
$check(count($GLOBALS['wechat_event_hooks']['jio_wechat_comment_parent'] ?? []) === 1, 'comment hook registered twice');
|
||||
|
||||
do_action('jio_order_paid', 'O-1', [
|
||||
'user_id' => 1,
|
||||
'product_id' => 9,
|
||||
'amount' => '12.50',
|
||||
'currency' => 'CNY',
|
||||
'paid_at' => '2026-11-09 09:00:00',
|
||||
]);
|
||||
$check(count($sender->messages) === 3, 'order should notify buyer and two admins once');
|
||||
$check($sender->messages[0]['type'] === 'payment_order', 'buyer order type');
|
||||
$check($sender->messages[0]['values'] === [
|
||||
'name' => 'Product 9', 'price' => '12.50 CNY', 'time' => '2026-11-09 09:00:00', 'num' => 'O-1',
|
||||
], 'buyer order fields');
|
||||
$check($sender->messages[0]['url'] === 'https://site.test/user/orders/', 'buyer order URL');
|
||||
$check($sender->messages[1]['type'] === 'payment_order_admin', 'admin order type');
|
||||
$check($sender->messages[1]['values']['user'] === 'User 1', 'admin buyer field');
|
||||
$check($sender->messages[1]['url'] === 'https://site.test/wp-admin/admin.php?page=jio_orders&order=O-1', 'admin order URL');
|
||||
|
||||
$sender->messages = [];
|
||||
$row = ['gross_amount' => '20.00', 'amount' => '19.00', 'created_at' => '2026-11-08 08:00:00'];
|
||||
do_action('jio_withdrawal_submitted', 1, $row);
|
||||
$check(count($sender->messages) === 2, 'withdraw request should notify admins');
|
||||
$check($sender->messages[0]['type'] === 'apply_withdraw_admin', 'withdraw request type');
|
||||
$check($sender->messages[0]['values']['price'] === '20.00 CNY', 'withdraw request amount');
|
||||
$check(str_contains($sender->messages[0]['url'], 'page=jio_withdraw'), 'withdraw admin URL');
|
||||
$sender->messages = [];
|
||||
do_action('jio_withdrawal_status_changed', 1, $row, \Jio\Payment\Withdrawal::STATUS_PROCESSING, '');
|
||||
$check($sender->messages === [], 'intermediate withdrawal status should not send');
|
||||
do_action('jio_withdrawal_status_changed', 1, $row, \Jio\Payment\Withdrawal::STATUS_COMPLETED, '');
|
||||
$check(count($sender->messages) === 1, 'processed withdrawal should send once');
|
||||
$check($sender->messages[0]['type'] === 'withdraw_process', 'processed withdrawal type');
|
||||
$check($sender->messages[0]['values']['service_price'] === '1.00 CNY', 'withdraw fee field');
|
||||
$check($sender->messages[0]['values']['received_price'] === '19.00 CNY', 'withdraw received field');
|
||||
|
||||
$sender->messages = [];
|
||||
$comment = new WP_Comment(11, 9, '2026-11-10 09:00:00', 'Hello');
|
||||
do_action('jio_wechat_comment_postauthor', 1, $comment);
|
||||
do_action('jio_wechat_comment_parent', 2, $comment);
|
||||
$check(count($sender->messages) === 2, 'comment should produce two distinct event types');
|
||||
$check($sender->messages[0]['type'] === 'comment_to_postauthor', 'post author comment type');
|
||||
$check($sender->messages[0]['values']['post'] === 'Product 9', 'post title field');
|
||||
$check($sender->messages[1]['type'] === 'comment_to_parent', 'parent comment type');
|
||||
$check($sender->messages[1]['values']['content'] === 'Hello', 'reply content field');
|
||||
$check($sender->messages[1]['url'] === 'https://site.test/post/#comment-11', 'comment URL');
|
||||
|
||||
$sender->messages = [];
|
||||
$command = new \Jio\Ledger\LedgerCommand('income-1', 'creator_income', [], 'revenue', '12', 0, ['order_number' => 'O-2']);
|
||||
$entry = ['user_id' => 1, 'asset' => 'balance', 'delta_minor' => 250, 'entry_type' => 'credit'];
|
||||
do_action('jio_asset_transaction_committed', new \Jio\Ledger\LedgerResult(1, true, [$entry], []), $command);
|
||||
$check($sender->messages === [], 'replayed ledger result should not send');
|
||||
do_action('jio_asset_transaction_committed', new \Jio\Ledger\LedgerResult(2, false, [$entry], []), $command);
|
||||
$check(count($sender->messages) === 1, 'committed income should send once');
|
||||
$check($sender->messages[0]['type'] === 'payment_order_to_income', 'income type');
|
||||
$check($sender->messages[0]['values']['price'] === '2.50 CNY', 'income amount');
|
||||
$check($sender->messages[0]['values']['num'] === 'O-2', 'income order number');
|
||||
$sender->messages = [];
|
||||
$command = new \Jio\Ledger\LedgerCommand('income-2', 'creator_income', [], 'revenue', '12', 0, ['order_number' => '12']);
|
||||
do_action('jio_asset_transaction_committed', new \Jio\Ledger\LedgerResult(3, false, [$entry], []), $command);
|
||||
$check(count($sender->messages) === 1 && $sender->messages[0]['values']['num'] === 'O-3', 'ledger record id resolved to order number');
|
||||
|
||||
echo "WeChat events harness passed\n";
|
||||
}
|
||||
@@ -1,151 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* WeChat template-message settings harness.
|
||||
*
|
||||
* Drives the REAL UserController REST callbacks (get_wechat_status /
|
||||
* update_wechat_settings) against WP stubs so the previously-broken routes are
|
||||
* covered end to end:
|
||||
* - GET /wechat/status returns the WeChatNotify status shape (masked secret);
|
||||
* - POST /wechat/settings persists through Integration's key whitelist;
|
||||
* - the *** mask keeps the stored secret instead of overwriting it;
|
||||
* - omitted fields fall back to current values; unknown keys are dropped;
|
||||
* - inputs are sanitized before storage.
|
||||
*
|
||||
* Usage: php wechat-settings-harness.php -> prints "WeChat settings harness passed".
|
||||
*/
|
||||
/** @package Jio */
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
|
||||
final class WP_Error {
|
||||
public function __construct(private string $code, private string $message, private array $data = []) {}
|
||||
public function get_error_message(): string { return $this->message; }
|
||||
public function get_error_data(): array { return $this->data; }
|
||||
}
|
||||
final class WP_REST_Response {
|
||||
public $data;
|
||||
public $status;
|
||||
public function __construct($data = null, int $status = 200) {
|
||||
$this->data = $data;
|
||||
$this->status = $status;
|
||||
}
|
||||
public function __construct(public $data = null, public int $status = 200) {}
|
||||
}
|
||||
|
||||
final class WP_REST_Request {
|
||||
private array $json;
|
||||
public function __construct(array $json = []) { $this->json = $json; }
|
||||
public function __construct(private array $json = []) {}
|
||||
public function get_json_params(): array { return $this->json; }
|
||||
public function get_method(): string { return 'POST'; }
|
||||
}
|
||||
|
||||
// ---------- WP function stubs ----------
|
||||
function __($s, $d = null) { return (string) $s; }
|
||||
function sanitize_text_field($s) { return trim(strip_tags((string) $s)); }
|
||||
function current_user_can($c) { return true; }
|
||||
function wp_send_json_success($d) { echo (string) json_encode($d); }
|
||||
function get_transient($k) { return $GLOBALS['wechat_transient'] ?? false; }
|
||||
function get_option($k, $default = false) { return $GLOBALS['wechat_options'][$k] ?? $default; }
|
||||
function update_option($k, $v) { $GLOBALS['wechat_options'][$k] = $v; return true; }
|
||||
|
||||
$GLOBALS['wechat_options'] = ['jio_wechat_settings' => [
|
||||
'enabled' => true,
|
||||
'app_id' => 'wxAPPID',
|
||||
'app_secret' => 'SECRET123',
|
||||
'template_id' => 'TPL1',
|
||||
]];
|
||||
$GLOBALS['wechat_transient'] = 'ACCESS_TOKEN';
|
||||
function __($text, $domain = '') { return $text; }
|
||||
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
|
||||
function sanitize_key($value): string { return strtolower(preg_replace('/[^a-z0-9_-]/', '', (string) $value)); }
|
||||
function is_wp_error($value): bool { return $value instanceof WP_Error; }
|
||||
function get_option($key, $default = false) { return $GLOBALS['wechat_options'][$key] ?? $default; }
|
||||
function update_option($key, $value): bool { $GLOBALS['wechat_options'][$key] = $value; return true; }
|
||||
function get_current_user_id(): int { return 9; }
|
||||
function get_user_meta($user_id, $key, $single = false) { return $key === 'jio_weixingzh_id' && $user_id === 9 ? ($GLOBALS['wechat_openid'] ?? '') : ''; }
|
||||
function home_url($path = ''): string { return 'https://example.test' . $path; }
|
||||
function esc_url_raw($url): string { return (string) $url; }
|
||||
function current_time($format): string { return '2026-11-10 10:00:00'; }
|
||||
function wp_json_encode($value): string { return json_encode($value, JSON_UNESCAPED_UNICODE); }
|
||||
function wp_remote_post($url, $args) { $GLOBALS['wechat_last_post'] = [$url, $args]; return $GLOBALS['wechat_remote_response']; }
|
||||
function wp_remote_retrieve_body($response): string { return (string) ($response['body'] ?? ''); }
|
||||
function wp_remote_retrieve_response_code($response): int { return (int) ($response['response']['code'] ?? 0); }
|
||||
function get_userdata($user_id) { return null; }
|
||||
function get_users($args) { return []; }
|
||||
function add_action($hook, $callback, $priority = 10, $accepted_args = 1): void {}
|
||||
|
||||
$GLOBALS['wpdb'] = new class {
|
||||
public $usermeta = 'wp_usermeta';
|
||||
public function get_var($sql) { return 7; }
|
||||
public string $usermeta = 'wp_usermeta';
|
||||
public function get_var($query): int { return 1; }
|
||||
};
|
||||
$GLOBALS['wechat_options'] = ['jio_wechat_settings' => []];
|
||||
$GLOBALS['wechat_openid'] = 'OPENID9';
|
||||
$GLOBALS['wechat_remote_response'] = ['response' => ['code' => 200], 'body' => '{"errcode":0,"errmsg":"ok"}'];
|
||||
}
|
||||
|
||||
namespace Jio\Auth {
|
||||
final class WeixinGzh {
|
||||
public static function is_enabled(): bool { return true; }
|
||||
public static function get_instance(): self { return new self(); }
|
||||
public function access_token(): string { return 'ACCESS_TOKEN'; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Jio\Settings {
|
||||
// Settings 层仅为薄委托,harness 直接对接真实 Integration。
|
||||
class Settings {
|
||||
public static function get_wechat_settings(): array { return Integration::get_wechat_settings(); }
|
||||
public static function update_wechat_settings(array $settings): void { Integration::update_wechat_settings($settings); }
|
||||
public static function update_wechat_settings(array $config): void { Integration::update_wechat_settings($config); }
|
||||
public static function get_weixingzh_settings(): array { return ['gzh_type' => 'service', 'appid' => 'wxAPP', 'appsecret' => 'secret']; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/ControllerBase.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerReportOperations.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerAuthPhoneOperations.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerRestrictionOperations.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerFollowOperations.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserControllerProfileMediaOperations.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Admin/UserController.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Settings/Integration.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Settings/DomainGroups.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Payment/WeChatNotify.php';
|
||||
|
||||
$failures = [];
|
||||
$check = static function (string $label, bool $ok) use (&$failures): void {
|
||||
if (!$ok) {
|
||||
$failures[] = $label;
|
||||
}
|
||||
};
|
||||
$stored = static function (): array {
|
||||
$v = get_option('jio_wechat_settings', []);
|
||||
return is_array($v) ? $v : [];
|
||||
};
|
||||
|
||||
$controller = \Jio\Admin\UserController::get_instance();
|
||||
|
||||
// 1) GET /wechat/status 返回完整状态形状(密钥掩码)。
|
||||
$response = $controller->get_wechat_status(new WP_REST_Request());
|
||||
$data = is_array($response->data) ? ($response->data['data'] ?? []) : [];
|
||||
$check('status 含 enabled', array_key_exists('enabled', $data) && !empty($data['enabled']));
|
||||
$check('status 含 appid', ($data['appid'] ?? '') === 'wxAPPID');
|
||||
$check('status configured', !empty($data['configured']));
|
||||
$check('status has_token', !empty($data['has_token']));
|
||||
$check('status has_template', !empty($data['has_template']));
|
||||
$check('status bound_users', ($data['bound_users'] ?? 0) === 7);
|
||||
$check('status 掩码 app_secret', ($data['app_secret'] ?? '') === '***');
|
||||
$check('status allowed_types 非空', !empty($data['allowed_types']));
|
||||
|
||||
// 2) 完整更新写入全部字段。
|
||||
$response = $controller->update_wechat_settings(new WP_REST_Request([
|
||||
'enabled' => true,
|
||||
'app_id' => 'wxNEW',
|
||||
'app_secret' => 'NEWSECRET',
|
||||
'template_id' => 'TPL2',
|
||||
]));
|
||||
$check('更新写入 app_id', ($stored()['app_id'] ?? '') === 'wxNEW');
|
||||
$check('更新写入 app_secret', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||
$check('更新写入 template_id', ($stored()['template_id'] ?? '') === 'TPL2');
|
||||
$check('更新返回成功消息', str_contains((string) ($response->data['message'] ?? ''), '已保存'));
|
||||
|
||||
// 3) 掩码 *** 保留已存密钥。
|
||||
$controller->update_wechat_settings(new WP_REST_Request([
|
||||
'enabled' => true,
|
||||
'app_id' => 'wxNEW',
|
||||
'app_secret' => '***',
|
||||
]));
|
||||
$check('掩码保留原密钥', ($stored()['app_secret'] ?? '') === 'NEWSECRET');
|
||||
|
||||
// 4) 缺省字段沿用当前值,开关可关闭。
|
||||
$controller->update_wechat_settings(new WP_REST_Request(['enabled' => false]));
|
||||
$check('缺省字段沿用当前值', ($stored()['app_id'] ?? '') === 'wxNEW' && ($stored()['template_id'] ?? '') === 'TPL2');
|
||||
$check('enabled 可关闭', empty($stored()['enabled']));
|
||||
|
||||
// 5) 输入消毒 + 未知键被白名单丢弃。
|
||||
$controller->update_wechat_settings(new WP_REST_Request([
|
||||
'app_id' => '<b>wxXSS</b>',
|
||||
'evil' => 'x',
|
||||
]));
|
||||
$check('app_id 经消毒', ($stored()['app_id'] ?? '') === 'wxXSS');
|
||||
$check('未知键被忽略', !array_key_exists('evil', $stored()));
|
||||
|
||||
// 6) 未配置时空密钥可显式清空。
|
||||
update_option('jio_wechat_settings', ['enabled' => true, 'app_id' => 'a', 'app_secret' => '', 'template_id' => '']);
|
||||
$controller->update_wechat_settings(new WP_REST_Request(['app_secret' => '']));
|
||||
$check('空密钥可清存', ($stored()['app_secret'] ?? '') === '');
|
||||
|
||||
if ($failures) {
|
||||
echo "WeChat settings harness FAILED\n";
|
||||
foreach ($failures as $failure) {
|
||||
echo ' - ' . $failure . "\n";
|
||||
}
|
||||
exit(1);
|
||||
$root = dirname(__DIR__, 2);
|
||||
foreach ([
|
||||
'src/Admin/ControllerBase.php',
|
||||
'src/Admin/UserControllerReportOperations.php',
|
||||
'src/Admin/UserControllerAuthPhoneOperations.php',
|
||||
'src/Admin/UserControllerRestrictionOperations.php',
|
||||
'src/Admin/UserControllerFollowOperations.php',
|
||||
'src/Admin/UserControllerProfileMediaOperations.php',
|
||||
'src/Admin/UserController.php',
|
||||
'src/Settings/WechatTemplates.php',
|
||||
'src/Settings/Integration.php',
|
||||
'src/Payment/WeChatNotifyEvents.php',
|
||||
'src/Payment/WeChatNotify.php',
|
||||
] as $file) {
|
||||
require_once $root . '/' . $file;
|
||||
}
|
||||
|
||||
$check = static function (bool $condition, string $name): void {
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAILED: {$name}\n");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
$controller = \Jio\Admin\UserController::get_instance();
|
||||
$sender = \Jio\Payment\WeChatNotify::get_instance();
|
||||
$status = $controller->get_wechat_status(new WP_REST_Request())->data['data'];
|
||||
$check(count($status['types']) === 14, 'available types');
|
||||
$check($status['bound_users'] === 1 && $status['current_user_bound'], 'real binding key');
|
||||
$check($status['oauth_ready'], 'oauth status');
|
||||
|
||||
$config = ['enabled' => true, 'templates' => [
|
||||
'comment_to_parent' => ['enabled' => true, 'id' => 'TPL_REPLY', 'keys' => ['name' => 'thing1', 'time' => 'time2', 'content' => '']],
|
||||
]];
|
||||
$saved = $controller->update_wechat_settings(new WP_REST_Request($config));
|
||||
$check($saved->status === 200, 'save accepted');
|
||||
$stored = get_option('jio_wechat_settings');
|
||||
$check($stored['templates']['comment_to_parent']['id'] === 'TPL_REPLY', 'template persisted');
|
||||
$check(count($stored['templates']) === 14, 'all types normalized');
|
||||
$check(!isset($stored['app_id']) && !isset($stored['app_secret']) && !isset($stored['template_id']), 'obsolete settings removed');
|
||||
$check($controller->get_wechat_status(new WP_REST_Request())->data['data']['config'] === $stored, 'round trip');
|
||||
|
||||
$invalid = $controller->update_wechat_settings(new WP_REST_Request(['enabled' => true, 'templates' => [
|
||||
'comment_to_parent' => ['enabled' => true, 'id' => 'TPL', 'keys' => ['name' => 'bad key']],
|
||||
]]));
|
||||
$check($invalid->status === 400 && get_option('jio_wechat_settings') === $stored, 'invalid mapping rejected without write');
|
||||
$invalid = $controller->update_wechat_settings(new WP_REST_Request(['enabled' => true, 'templates' => ['unknown' => ['enabled' => true, 'id' => 'X']]]));
|
||||
$check($invalid->status === 400, 'unknown type rejected');
|
||||
$invalid = $controller->update_wechat_settings(new WP_REST_Request(['enabled' => true, 'templates' => [
|
||||
'comment_to_parent' => ['enabled' => true, 'id' => 'TPL', 'keys' => ['name' => 'keyword3']],
|
||||
]]));
|
||||
$check($invalid->status === 400 && get_option('jio_wechat_settings') === $stored, 'fallback mapping collision rejected');
|
||||
$draft = $controller->update_wechat_settings(new WP_REST_Request(['enabled' => true, 'templates' => [
|
||||
'comment_to_parent' => ['enabled' => true, 'id' => '', 'keys' => []],
|
||||
]]));
|
||||
$check($draft->status === 200, 'incomplete template can be saved as draft');
|
||||
$blocked = $controller->test_wechat_template(new WP_REST_Request(['type' => 'comment_to_parent']));
|
||||
$check($blocked->status === 400, 'incomplete template cannot be tested');
|
||||
$controller->update_wechat_settings(new WP_REST_Request($config));
|
||||
|
||||
$sent = $sender->send_event(9, 'comment_to_parent', ['name' => str_repeat('中', 25), 'time' => '2026-11-10', 'content' => '测试回复'], 'https://example.test/comment/1');
|
||||
$body = json_decode($GLOBALS['wechat_last_post'][1]['body'], true);
|
||||
$check($sent, 'send succeeds');
|
||||
$check($body['touser'] === 'OPENID9' && $body['template_id'] === 'TPL_REPLY', 'recipient and template');
|
||||
$check($body['url'] === 'https://example.test/comment/1', 'deep link');
|
||||
$check($body['data']['thing1']['value'] === str_repeat('中', 16) . '...', 'thing truncation');
|
||||
$check($body['data']['time2']['value'] === '2026-11-10' && $body['data']['keyword3']['value'] === '测试回复', 'mapping and fallback');
|
||||
|
||||
$tested = $controller->test_wechat_template(new WP_REST_Request(['type' => 'comment_to_parent']));
|
||||
$check($tested->status === 200, 'test endpoint sends');
|
||||
$GLOBALS['wechat_remote_response'] = ['response' => ['code' => 200], 'body' => '{"errcode":40013,"errmsg":"invalid appid"}'];
|
||||
$tested = $controller->test_wechat_template(new WP_REST_Request(['type' => 'comment_to_parent']));
|
||||
$check($tested->status === 502 && str_contains($tested->data['message'], '40013'), 'api failure exposed');
|
||||
$GLOBALS['wechat_openid'] = '';
|
||||
$tested = $controller->test_wechat_template(new WP_REST_Request(['type' => 'comment_to_parent']));
|
||||
$check($tested->status === 400, 'unbound user rejected');
|
||||
|
||||
echo "WeChat settings harness passed\n";
|
||||
}
|
||||
@@ -1,84 +1,44 @@
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* 微信模板消息 REST 契约(修复存量审计错误):
|
||||
* 1) /wechat/status 与 /wechat/settings 的路由回调方法在 UserController 中真实存在;
|
||||
* 2) 状态读取委托 WeChatNotify::get_status(),写入委托 Settings::update_wechat_settings();
|
||||
* 3) app_secret 掩码 *** 不覆盖已存密钥,输入经 sanitize_text_field 消毒;
|
||||
* 4) PHP harness 驱动真实控制器验证上述行为;
|
||||
* 5) jio_settings 上的微信通知死键(wechat_notify_* 等)已彻底移除,
|
||||
* 活配置面仅剩 jio_wechat_settings(/users/wechat-template 自定义页)。
|
||||
*/
|
||||
const assert = require('node:assert/strict')
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const test = require('node:test')
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||||
const controller = read('src/Admin/UserController.php')
|
||||
const sender = read('src/Payment/WeChatNotify.php')
|
||||
const settings = read('src/Settings/Integration.php')
|
||||
const page = read('assets/admin/src/pages/users/UsersWechatTemplate.vue')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const controller = fs.readFileSync(path.join(root, 'src/Admin/UserController.php'), 'utf8');
|
||||
const schemaUsers = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
|
||||
const usersSchemaIndex = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/index.js'), 'utf8');
|
||||
const usersSections = fs.readFileSync(path.join(root, 'assets/admin/src/constants/sections/users.js'), 'utf8');
|
||||
|
||||
// 死双路面:写进 jio_settings、全仓无消费方的微信通知键。
|
||||
const DEAD_WECHAT_KEYS = [
|
||||
'wechat_notify_enabled',
|
||||
'wechat_app_id',
|
||||
'wechat_app_secret',
|
||||
'wechat_template_id',
|
||||
'wechat_token',
|
||||
'wechat_encoding_aes_key',
|
||||
'wechat_template_comment',
|
||||
'wechat_template_reply',
|
||||
'wechat_template_system',
|
||||
'wechat_template_order',
|
||||
'wechat_notify_comment',
|
||||
'wechat_notify_reply',
|
||||
'wechat_notify_system',
|
||||
'wechat_notify_order',
|
||||
'wechat_bind_entry',
|
||||
];
|
||||
|
||||
test('路由与回调方法配对存在', () => {
|
||||
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/status'[\s\S]*?'callback'\s*=>\s*\[\$this, 'get_wechat_status'\]/);
|
||||
assert.match(controller, /register_rest_route\(self::API_NAMESPACE, '\/wechat\/settings'[\s\S]*?'callback'\s*=>\s*\[\$this, 'update_wechat_settings'\]/);
|
||||
assert.match(controller, /public function get_wechat_status\(\\WP_REST_Request \$request\)/);
|
||||
assert.match(controller, /public function update_wechat_settings\(\\WP_REST_Request \$request\)/);
|
||||
});
|
||||
|
||||
test('状态与写入委托既有服务层', () => {
|
||||
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||
});
|
||||
|
||||
test('密钥掩码与输入消毒', () => {
|
||||
assert.match(controller, /\$params\['app_secret'\] !== '\*\*\*'/);
|
||||
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_id'\]\)/);
|
||||
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['app_secret'\]\)/);
|
||||
assert.match(controller, /sanitize_text_field\(\(string\) \$params\['template_id'\]\)/);
|
||||
});
|
||||
|
||||
test('微信设置行为 harness 通过', () => {
|
||||
const harness = path.join(root, 'tests', 'admin', 'wechat-settings-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /WeChat settings harness passed/);
|
||||
});
|
||||
|
||||
test('死双路面(jio_settings 微信通知键)已彻底移除', () => {
|
||||
for (const key of DEAD_WECHAT_KEYS) {
|
||||
assert.ok(!schemaUsers.includes(key), `SchemaUsers 残留: ${key}`);
|
||||
assert.ok(!usersSections.includes(key), `users 菜单常量残留: ${key}`);
|
||||
test('设置、状态和测试接口均受后台权限约束', () => {
|
||||
for (const route of ['/wechat/status', '/wechat/settings', '/wechat/test']) {
|
||||
assert.match(controller, new RegExp(`register_rest_route\\(self::API_NAMESPACE, '${route}'[\\s\\S]*?'permission_callback'\\s*=>\\s*\\[\\$this, 'check_permission'\\]`))
|
||||
}
|
||||
assert.ok(!usersSchemaIndex.includes('wechatSchema'), 'users schema index 残留 wechatSchema 注册');
|
||||
assert.ok(!usersSections.includes("route: '/users/wechat',"), '用户菜单残留微信通知 schema 页');
|
||||
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/schema/sections/users/wechat.js')), 'wechat.js 未删除');
|
||||
});
|
||||
})
|
||||
|
||||
test('活配置面保持唯一(jio_wechat_settings + 自定义页)', () => {
|
||||
assert.match(usersSections, /key: 'users-wechat-template'/);
|
||||
assert.match(usersSections, /route: '\/users\/wechat-template'/);
|
||||
assert.match(controller, /WeChatNotify::get_instance\(\)->get_status\(\)/);
|
||||
assert.match(controller, /Settings::update_wechat_settings\(/);
|
||||
});
|
||||
test('配置与发送共用公众号登录及已绑定账号', () => {
|
||||
assert.match(sender, /Settings::get_weixingzh_settings\(\)/)
|
||||
assert.match(sender, /WeixinGzh::get_instance\(\)->access_token\(\)/)
|
||||
assert.match(sender, /jio_weixingzh_id/)
|
||||
assert.doesNotMatch(sender, /jio_wechat_openid|jio_wechat_subscriptions|set_transient\('jio_wechat_token'/)
|
||||
assert.match(settings, /WechatTemplates::normalize\(\$stored\)/)
|
||||
assert.doesNotMatch(settings, /jio_wechat_settings'[\s\S]{0,350}'app_secret'/)
|
||||
})
|
||||
|
||||
test('页面提供分类型设置和发送测试', () => {
|
||||
assert.match(page, /form\.templates\[type\.key\]\.enabled/)
|
||||
assert.match(page, /form\.templates\[type\.key\]\.id/)
|
||||
assert.match(page, /form\.templates\[type\.key\]\.keys\[field\.key\]/)
|
||||
assert.match(page, /\/wechat\/test/)
|
||||
assert.match(page, /data: JSON\.parse\(snapshot\.value\)/)
|
||||
assert.doesNotMatch(page, /structuredClone\(form\)/)
|
||||
assert.doesNotMatch(page, /form\.app_secret|form\.template_id|alert\(/)
|
||||
})
|
||||
|
||||
test('PHP 行为验证', () => {
|
||||
const output = execFileSync('php', [path.join(root, 'tests/admin/wechat-settings-harness.php')], { encoding: 'utf8' })
|
||||
assert.match(output, /WeChat settings harness passed/)
|
||||
})
|
||||
@@ -544,12 +544,10 @@ test('report modal delegates its labelled title, close button and focus return t
|
||||
assert.doesNotMatch(reportJs, /class="close"|style="display:|setTimeout/)
|
||||
})
|
||||
|
||||
test('wechat template messages cover report and account restriction types', () => {
|
||||
const wechat = read('src/Payment/WeChatNotify.php')
|
||||
assert.match(wechat, /const NOTIFY_TYPES = \['comment_reply', 'system_notice', 'order_paid', 'credits_change', 'report', 'system'\]/)
|
||||
assert.match(wechat, /in_array\(\$type, self::NOTIFY_TYPES, true\)/)
|
||||
// 订阅白名单与状态输出复用同一常量,不允许再出现硬编码子集。
|
||||
assert.ok(!wechat.includes("['comment_reply', 'system_notice', 'order_paid', 'credits_change']"), 'type list must come from NOTIFY_TYPES')
|
||||
// 站内通知缺省模板字段时用 title/description 兜底合成。
|
||||
assert.match(wechat, /\$data\['description'\] \?\? ''/)
|
||||
test('wechat report templates use distinct submission and processing events', () => {
|
||||
const events = read('src/Payment/WeChatNotifyEvents.php')
|
||||
assert.match(events, /add_action\('jio_report_submitted', \[\$this, 'on_wechat_report_submitted'\]/)
|
||||
assert.match(events, /add_action\('jio_report_status_changed', \[\$this, 'on_wechat_report_processed'\]/)
|
||||
assert.match(events, /send_wechat_admins\('report_user_admin'/)
|
||||
assert.match(events, /send_event\(\$reporter_id, 'report_process'/)
|
||||
})
|
||||
Reference in new issue
Block a user