fix(users): restore WeChat template notification flow

This commit is contained in:
kate522 committed 2026-11-10 20:54:24 +08:00
1 parent 30ceab4615
commit 7fe26d3fff
18 files changed
+1051 -763

No files matched your search

+115 -125
View File
@@ -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";
}