AgentPageConfigurationTest.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\AgentPageConfiguration;
  4. use App\Services\Agent\AgentPageConfigurationService;
  5. use Illuminate\Config\Repository;
  6. use Illuminate\Database\Capsule\Manager;
  7. use Illuminate\Events\Dispatcher;
  8. use Illuminate\Foundation\Application;
  9. use Illuminate\Http\JsonResponse;
  10. use Illuminate\Http\Request;
  11. use Illuminate\Routing\Router;
  12. use Illuminate\Support\Facades\DB;
  13. use Illuminate\Support\Facades\Facade;
  14. use Illuminate\Translation\ArrayLoader;
  15. use Illuminate\Translation\Translator;
  16. use Illuminate\Validation\Factory;
  17. use Illuminate\Validation\ValidationException;
  18. use PHPUnit\Framework\TestCase;
  19. class AgentPageConfigurationTest extends TestCase
  20. {
  21. private Application $app;
  22. private AgentPageConfigurationService $service;
  23. protected function setUp(): void
  24. {
  25. Facade::clearResolvedInstances(); $this->app = new Application(dirname(__DIR__, 2));
  26. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
  27. Facade::setFacadeApplication($this->app);
  28. $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  29. $db->setEventDispatcher(new Dispatcher($this->app)); $db->setAsGlobal(); $db->bootEloquent();
  30. $this->app->instance('db', $db->getDatabaseManager()); $this->app->bind('db.schema', fn () => $db->schema());
  31. $translator = new Translator(new ArrayLoader(), 'zh'); $this->app->instance('translator', $translator);
  32. $this->app->instance('validator', new Factory($translator, $this->app));
  33. $this->app->instance('log', new \Psr\Log\NullLogger());
  34. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class {
  35. public function json($data, $status = 200, $headers = [], $options = 0) { return new JsonResponse($data, $status, $headers, $options); }
  36. });
  37. $request = Request::create('/admin/agent/pageConfig'); $request->user = (object)['id' => 1, 'username' => 'admin']; $this->app->instance('request', $request);
  38. DB::statement('CREATE TABLE bot_operation_audits (id INTEGER PRIMARY KEY AUTOINCREMENT,resource_type TEXT,resource_id INTEGER,action TEXT,changes TEXT,operator_id INTEGER,operator_name TEXT,created_at TEXT)');
  39. $this->migration()->up();
  40. $this->service = new AgentPageConfigurationService();
  41. }
  42. protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
  43. private function migration() { return require dirname(__DIR__, 2) . '/database/migrations/2026_10_30_180000_create_agent_page_settings.php'; }
  44. private function emptyConfig(): array { return ['commission_plan_html' => '', 'contact_tg_url' => '', 'agent_backend_url' => '']; }
  45. private function payload(): array
  46. {
  47. return ['commission_plan_html' => '<p><strong>佣金方案 / Tiếng Việt</strong></p><table style="width:100%;border-collapse:collapse" border="1"><caption>代理比例</caption><colgroup><col width="50%"></colgroup><thead><tr><th colspan="2">等级</th></tr></thead><tbody><tr><td rowspan="2" style="text-align:center;color:#f00">一级代理</td><td>30%</td></tr><tr><td>40%</td></tr></tbody></table>',
  48. 'contact_tg_url' => ' https://t.me/customer_service ', 'agent_backend_url' => 'https://agent.example.com/login?from=member#start'];
  49. }
  50. public function test_defaults_and_migration_can_be_rolled_back_and_reapplied(): void
  51. {
  52. $this->assertSame($this->emptyConfig(), $this->service->get());
  53. $this->migration()->down(); $this->migration()->up();
  54. $this->assertSame($this->emptyConfig(), $this->service->get());
  55. }
  56. public function test_rich_table_and_two_urls_save_together_and_only_public_fields_are_returned(): void
  57. {
  58. $this->app['request']->merge($this->payload());
  59. $response = (new AgentPageConfiguration())->save($this->service);
  60. $result = $response->getData(true); $this->assertSame(0, $result['code']);
  61. $data = $result['data'];
  62. $this->assertSame(array_keys($this->emptyConfig()), array_keys($data));
  63. $this->assertSame('https://t.me/customer_service', $data['contact_tg_url']);
  64. $this->assertSame($this->payload()['agent_backend_url'], $data['agent_backend_url']);
  65. foreach (['<table', '<thead>', '<tbody>', '<caption>', '<colgroup>', 'colspan="2"', 'rowspan="2"', 'width: 100%', 'text-align: center', 'color: #f00', '佣金方案', 'Tiếng Việt', '30%', '40%'] as $text) $this->assertStringContainsString($text, $data['commission_plan_html']);
  66. $this->assertSame($data, $this->service->get());
  67. $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control'));
  68. $this->assertSame(1, DB::table('agent_page_settings')->count());
  69. $this->assertSame(1, DB::table('operation_audits')->count());
  70. $this->assertSame($data, $this->service->save($data));
  71. $this->assertSame(1, DB::table('operation_audits')->count());
  72. $this->assertSame($this->emptyConfig(), $this->service->save($this->emptyConfig()));
  73. }
  74. public function test_active_markup_and_unsafe_styles_are_removed_without_flattening_tables(): void
  75. {
  76. $input = $this->emptyConfig();
  77. $input['commission_plan_html'] = '<script>alert(1)</script><svg><script>alert(2)</script></svg><iframe src="https://evil.example"></iframe><p onclick="alert(3)">中文</p><table><tr><td style="color:red;position:fixed;background-color:url(https://evil.example);width:expression(alert(4))" onmouseover="alert(5)">30%</td></tr></table><a href="jav&#x61;script:alert(6)">错误链接</a><a href="https://t.me/customer_service" target="_blank">TG</a>';
  78. $html = $this->service->save($input)['commission_plan_html'];
  79. foreach (['<script', '<svg', '<iframe', 'onclick', 'onmouseover', 'javascript:', 'expression(', 'url(', 'position:', 'alert('] as $danger) $this->assertStringNotContainsString($danger, $html);
  80. foreach (['<table>', '<td style="color: red">30%</td>', '中文', 'href="https://t.me/customer_service"', 'rel="noopener noreferrer"'] as $safe) $this->assertStringContainsString($safe, $html);
  81. }
  82. /** @dataProvider invalidValues */
  83. public function test_invalid_values_do_not_partially_save(string $field, $invalid): void
  84. {
  85. $before = $this->service->save($this->payload());
  86. $input = $before; $input[$field] = $invalid;
  87. try { $this->service->save($input); $this->fail('Expected validation failure'); }
  88. catch (ValidationException $e) { $this->assertArrayHasKey($field, $e->errors()); }
  89. $this->assertSame($before, $this->service->get()); $this->assertSame(1, DB::table('operation_audits')->count());
  90. }
  91. public static function invalidValues(): array
  92. {
  93. $cases = [['commission_plan_html', []], ['commission_plan_html', str_repeat('字', 100001)]];
  94. foreach (['contact_tg_url', 'agent_backend_url'] as $field) {
  95. foreach (['javascript:alert(1)', '//example.com', 'https://user:password@example.com', "https://example.com/\na", 'https://example.com/\\evil', str_repeat('x', 2049), ['url']] as $value) $cases[] = [$field, $value];
  96. }
  97. return $cases;
  98. }
  99. public function test_save_is_atomic_when_audit_fails_and_missing_form_fields_are_rejected(): void
  100. {
  101. $before = $this->service->save($this->payload());
  102. foreach (array_keys($before) as $field) {
  103. $input = $before; unset($input[$field]);
  104. try { $this->service->save($input); $this->fail('Missing field accepted'); }
  105. catch (ValidationException $e) { $this->assertArrayHasKey($field, $e->errors()); }
  106. }
  107. DB::statement('DROP TABLE bot_operation_audits');
  108. try { $this->service->save($this->emptyConfig()); $this->fail('Audit failure was swallowed'); }
  109. catch (\Illuminate\Database\QueryException $e) { $this->assertSame($before, $this->service->get()); }
  110. }
  111. public function test_admin_routes_keep_login_agent_admin_and_read_write_permissions(): void
  112. {
  113. $router = new Router(new Dispatcher($this->app), $this->app); $this->app->instance('router', $router);
  114. $router->group(['prefix' => 'admin'], function () { require dirname(__DIR__, 2) . '/routes/admin.php'; });
  115. foreach (['GET' => ['show', 'admin/agent'], 'POST' => ['save', 'admin/agent/update']] as $method => [$action, $permission]) {
  116. $route = $router->getRoutes()->match(Request::create('/admin/agent/pageConfig', $method));
  117. $this->assertSame(AgentPageConfiguration::class . '@' . $action, $route->getAction('uses'));
  118. foreach (['admin.jwt', 'check.button.uri', 'agent.admin'] as $middleware) $this->assertContains($middleware, $route->middleware());
  119. $this->assertSame($permission, $route->defaults['permission_uri']);
  120. }
  121. $result = (new AgentPageConfiguration())->show($this->service)->getData(true);
  122. $this->assertSame($this->emptyConfig(), $result['data']);
  123. DB::statement('DROP TABLE bot_agent_page_settings');
  124. $failed = (new AgentPageConfiguration())->show($this->service)->getData(true);
  125. $this->assertSame('代理页面配置暂不可用,请稍后重试', $failed['msg']);
  126. }
  127. }