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
@@ -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";
|
||||
}
|
||||
Reference in new issue
Block a user