water_package_delete_test.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. <?php
  2. /** php tests/water_package_delete_test.php:所有测试数据在事务中回滚。 */
  3. require dirname(__DIR__) . '/vendor/autoload.php';
  4. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  5. \support\App::loadAllConfig(['route']);
  6. date_default_timezone_set('Asia/Shanghai');
  7. use app\miniapi\service\Catalog;
  8. use plugin\saiadmin\app\controller\water\WaterController;
  9. use plugin\saiadmin\app\model\water\WaterAccount;
  10. use plugin\saiadmin\app\model\water\WaterLedger;
  11. use plugin\saiadmin\app\model\water\WaterOperator;
  12. use plugin\saiadmin\app\model\water\WaterOrder;
  13. use plugin\saiadmin\app\model\water\WaterPackage;
  14. use plugin\saiadmin\app\model\water\WaterSku;
  15. use plugin\saiadmin\app\service\WaterBusiness;
  16. use plugin\saiadmin\exception\ApiException;
  17. use support\Request;
  18. $checks = 0;
  19. function check(bool $condition, string $message): void
  20. {
  21. global $checks;
  22. ++$checks;
  23. if (!$condition) {
  24. throw new RuntimeException($message);
  25. }
  26. }
  27. function rejects(callable $callback, string $message): void
  28. {
  29. try {
  30. $callback();
  31. } catch (ApiException $exception) {
  32. check(true, $message);
  33. return;
  34. }
  35. throw new RuntimeException($message);
  36. }
  37. function packageRequest(array $input): Request
  38. {
  39. $body = json_encode($input, JSON_THROW_ON_ERROR);
  40. return new Request("POST /core/water/packages HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: " . strlen($body) . "\r\n\r\n" . $body);
  41. }
  42. $service = new WaterBusiness();
  43. $tag = 'PKG-DELETE-' . bin2hex(random_bytes(4));
  44. $connection = (new WaterPackage())->getConnection();
  45. $before = WaterPackage::query()->orderBy('id')->get()->toArray();
  46. $beforeLedger = WaterLedger::query()->count();
  47. $beforeOrders = WaterOrder::query()->count();
  48. $connection->beginTransaction();
  49. try {
  50. $enterprise = $service->save('enterprise', ['name' => $tag, 'contact' => '测试', 'phone' => 'TEST', 'address' => '事务回滚地址'], 1)['id'];
  51. $sku = $service->save('sku', ['name' => $tag, 'liters' => 18.9], 1)['id'];
  52. $packageInput = ['sku_id' => $sku, 'buckets' => 30, 'price_cents' => 69900];
  53. $id = $service->save('package', ['name' => $tag . '-delete'] + $packageInput, 1)['id'];
  54. $keepId = $service->save('package', ['name' => $tag . '-keep'] + $packageInput, 1)['id'];
  55. $orderId = $service->purchase(['enterprise_id' => $enterprise, 'package_id' => $id, 'reference' => $tag], 1)['id'];
  56. $order = WaterOrder::query()->findOrFail($orderId)->toArray();
  57. $account = WaterAccount::query()->where('enterprise_id', $enterprise)->firstOrFail()->toArray();
  58. $stock = WaterSku::query()->findOrFail($sku)->toArray();
  59. $otherPackage = WaterPackage::query()->findOrFail($keepId)->toArray();
  60. $auditCount = WaterLedger::query()->count();
  61. $reflection = new ReflectionClass(WaterController::class);
  62. $controller = $reflection->newInstanceWithoutConstructor();
  63. $adminId = $reflection->getProperty('adminId');
  64. $adminId->setValue($controller, 0);
  65. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $id]), 'packages'), 'unauthenticated deletion rejected');
  66. $operator = WaterOperator::query()->insertGetId(['username' => $tag, 'password' => password_hash($tag, PASSWORD_DEFAULT), 'status' => 1]);
  67. $adminId->setValue($controller, (int) $operator);
  68. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $id]), 'packages'), 'operator without write permission rejected');
  69. $adminId->setValue($controller, 1);
  70. foreach ([null, 0, -1, 'invalid', 1.5, []] as $invalidId) {
  71. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $invalidId]), 'packages'), 'invalid package ID rejected');
  72. }
  73. $missingId = (int) WaterPackage::query()->max('id') + 100;
  74. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $missingId]), 'packages'), 'missing package rejected');
  75. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $enterprise]), 'enterprises'), 'delete action limited to packages');
  76. check(WaterPackage::query()->where('id', $id)->exists(), 'rejected requests leave package intact');
  77. check(WaterLedger::query()->count() === $auditCount, 'rejected requests create no audits');
  78. $response = json_decode($controller->save(packageRequest(['action' => 'delete', 'id' => (string) $id]), 'packages')->rawBody(), true, 512, JSON_THROW_ON_ERROR);
  79. check($response['code'] === 200 && $response['data']['id'] === $id, 'JSON delete returns selected ID');
  80. check(!WaterPackage::query()->where('id', $id)->exists(), 'selected package physically deleted');
  81. check(WaterPackage::query()->findOrFail($keepId)->toArray() === $otherPackage, 'other package unchanged');
  82. check(WaterOrder::query()->findOrFail($orderId)->toArray() === $order, 'historical order snapshot retained');
  83. check(WaterAccount::query()->findOrFail($account['id'])->toArray() === $account, 'enterprise entitlement retained');
  84. check(WaterSku::query()->findOrFail($sku)->toArray() === $stock, 'inventory unchanged');
  85. $listing = $service->listing('packages', ['keyword' => $tag]);
  86. check($listing['total'] === 1 && array_column($listing['data'], 'id') === [$keepId], 'admin listing excludes deleted package');
  87. $optionsRequest = new Request('GET /core/water/packages/options?type=package&keyword=' . $tag . " HTTP/1.1\r\nHost: localhost\r\n\r\n");
  88. $options = json_decode($controller->options($optionsRequest, 'packages')->rawBody(), true, 512, JSON_THROW_ON_ERROR);
  89. check(array_column($options['data'], 'id') === [$keepId], 'package selector excludes deleted package');
  90. check(array_column((new Catalog())->listing(['keyword' => $tag])['items'], 'id') === [$keepId], 'mini-program catalog excludes deleted package');
  91. $audit = WaterLedger::query()->where(['kind' => 'archive', 'reference' => 'package:' . $id])->orderByDesc('id')->firstOrFail();
  92. check($audit->note === '删除套餐:' . $tag . '-delete' && (int) $audit->created_by === 1, 'deletion audit records actor and package');
  93. check(WaterLedger::query()->count() === $auditCount + 1, 'one deletion audit added');
  94. rejects(fn () => $controller->save(packageRequest(['action' => 'delete', 'id' => $id]), 'packages'), 'repeated deletion reports missing package');
  95. check(WaterLedger::query()->count() === $auditCount + 1, 'repeated deletion creates no extra audit');
  96. } finally {
  97. $connection->rollBack();
  98. }
  99. check(WaterPackage::query()->orderBy('id')->get()->toArray() === $before, 'original package data restored after rollback');
  100. check(WaterLedger::query()->count() === $beforeLedger && WaterOrder::query()->count() === $beforeOrders, 'audit and order fixtures rolled back');
  101. check(!WaterOperator::query()->where('username', $tag)->exists() && !WaterSku::query()->where('name', $tag)->exists(), 'operator and inventory fixtures rolled back');
  102. if (in_array('--http', $argv, true)) {
  103. $token = \Tinywan\Jwt\JwtToken::generateToken(['id' => 1, 'username' => 'admin', 'plat' => 'saiadmin', 'type' => 'web', 'client' => $tag, 'access_exp' => 120])['access_token'];
  104. $curl = curl_init('http://127.0.0.1:8787/core/water/packages');
  105. curl_setopt_array($curl, [
  106. CURLOPT_POST => true,
  107. CURLOPT_POSTFIELDS => json_encode(['action' => 'delete', 'id' => 0], JSON_THROW_ON_ERROR),
  108. CURLOPT_RETURNTRANSFER => true,
  109. CURLOPT_TIMEOUT => 15,
  110. CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
  111. ]);
  112. $body = curl_exec($curl);
  113. $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  114. curl_close($curl);
  115. $response = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR);
  116. check($status === 200 && ($response['message'] ?? '') === '套餐ID必须是有效整数', 'live HTTP worker has loaded delete action; invalid ID changes no data');
  117. unset($token);
  118. }
  119. echo "PASS: $checks package deletion checks; all fixtures rolled back.\n";