From 659e56220833a4cb0891a1828e1bdca916bbaf94 Mon Sep 17 00:00:00 2001 From: lkddi Date: Fri, 3 Apr 2026 13:55:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95:=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=A8=B1=E4=B9=90=E6=A8=A1=E5=9D=97=20(Gomok?= =?UTF-8?q?u,=20HorseRace,=20Lottery=20=E7=AD=89)=20=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E8=81=94=E8=B0=83=E6=B5=8B=E8=AF=95=E4=B8=8E?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/AuthController.php | 10 +- app/Http/Requests/StoreRoomRequest.php | 2 +- app/Http/Requests/UpdateRoomRequest.php | 2 +- .../WechatBot/KafkaConsumerService.php | 32 ++- database/factories/UserFactory.php | 15 +- .../2026_02_27_074855_create_shop_tables.php | 2 +- ...26_02_28_141645_create_feedback_tables.php | 4 +- phpunit.xml | 4 +- tests/Feature/AuthControllerTest.php | 184 ++++++++++++ tests/Feature/BaccaratControllerTest.php | 200 +++++++++++++ tests/Feature/BankControllerTest.php | 154 ++++++++++ tests/Feature/ChatBotControllerTest.php | 125 ++++++++ tests/Feature/ChatControllerTest.php | 150 ++++++++++ tests/Feature/DutyHallControllerTest.php | 84 ++++++ tests/Feature/EarnControllerTest.php | 78 +++++ tests/Feature/ExampleTest.php | 4 +- tests/Feature/FishingControllerTest.php | 125 ++++++++ .../Feature/FortuneTellingControllerTest.php | 142 +++++++++ tests/Feature/FriendControllerTest.php | 169 +++++++++++ tests/Feature/GomokuControllerTest.php | 176 ++++++++++++ tests/Feature/GuestbookControllerTest.php | 173 +++++++++++ tests/Feature/HolidayControllerTest.php | 139 +++++++++ tests/Feature/HorseRaceControllerTest.php | 185 ++++++++++++ tests/Feature/InviteControllerTest.php | 43 +++ tests/Feature/LeaderboardControllerTest.php | 73 +++++ tests/Feature/LotteryControllerTest.php | 214 ++++++++++++++ tests/Feature/MarriageControllerTest.php | 269 ++++++++++++++++++ tests/Feature/RedPacketControllerTest.php | 165 +++++++++++ tests/Feature/RoomControllerTest.php | 201 +++++++++++++ tests/Feature/ShopControllerTest.php | 169 +++++++++++ tests/Feature/SlotMachineControllerTest.php | 143 ++++++++++ tests/Feature/UserControllerTest.php | 255 +++++++++++++++++ tests/Feature/WeddingControllerTest.php | 244 ++++++++++++++++ 33 files changed, 3907 insertions(+), 28 deletions(-) create mode 100644 tests/Feature/AuthControllerTest.php create mode 100644 tests/Feature/BaccaratControllerTest.php create mode 100644 tests/Feature/BankControllerTest.php create mode 100644 tests/Feature/ChatBotControllerTest.php create mode 100644 tests/Feature/ChatControllerTest.php create mode 100644 tests/Feature/DutyHallControllerTest.php create mode 100644 tests/Feature/EarnControllerTest.php create mode 100644 tests/Feature/FishingControllerTest.php create mode 100644 tests/Feature/FortuneTellingControllerTest.php create mode 100644 tests/Feature/FriendControllerTest.php create mode 100644 tests/Feature/GomokuControllerTest.php create mode 100644 tests/Feature/GuestbookControllerTest.php create mode 100644 tests/Feature/HolidayControllerTest.php create mode 100644 tests/Feature/HorseRaceControllerTest.php create mode 100644 tests/Feature/InviteControllerTest.php create mode 100644 tests/Feature/LeaderboardControllerTest.php create mode 100644 tests/Feature/LotteryControllerTest.php create mode 100644 tests/Feature/MarriageControllerTest.php create mode 100644 tests/Feature/RedPacketControllerTest.php create mode 100644 tests/Feature/RoomControllerTest.php create mode 100644 tests/Feature/ShopControllerTest.php create mode 100644 tests/Feature/SlotMachineControllerTest.php create mode 100644 tests/Feature/UserControllerTest.php create mode 100644 tests/Feature/WeddingControllerTest.php diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 6e29aac..0e8c5bc 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -39,7 +39,15 @@ class AuthController extends Controller if ($user) { // 用户存在,验证密码 - if (Hash::check($password, $user->password)) { + $passwordMatches = false; + try { + $passwordMatches = Hash::check($password, $user->password); + } catch (\RuntimeException $e) { + // Hash::check() in Laravel 11/12 throws if the hash isn't a valid bcrypt string + $passwordMatches = false; + } + + if ($passwordMatches) { // Bcrypt 验证通过 // 检测是否被封禁 (后台管理员级别获得豁免权,防止误把自己关在门外) diff --git a/app/Http/Requests/StoreRoomRequest.php b/app/Http/Requests/StoreRoomRequest.php index 0a0165f..e94d1a2 100644 --- a/app/Http/Requests/StoreRoomRequest.php +++ b/app/Http/Requests/StoreRoomRequest.php @@ -33,7 +33,7 @@ class StoreRoomRequest extends FormRequest public function rules(): array { return [ - 'name' => ['required', 'string', 'max:50', 'unique:rooms,name'], + 'name' => ['required', 'string', 'max:50', 'unique:rooms,room_name'], 'description' => ['nullable', 'string', 'max:255'], ]; } diff --git a/app/Http/Requests/UpdateRoomRequest.php b/app/Http/Requests/UpdateRoomRequest.php index 67b39ae..778e870 100644 --- a/app/Http/Requests/UpdateRoomRequest.php +++ b/app/Http/Requests/UpdateRoomRequest.php @@ -31,7 +31,7 @@ class UpdateRoomRequest extends FormRequest public function rules(): array { return [ - 'name' => ['required', 'string', 'max:50', 'unique:rooms,name,'.$this->route('id')], + 'name' => ['required', 'string', 'max:50', 'unique:rooms,room_name,'.$this->route('id')], 'description' => ['nullable', 'string', 'max:255'], ]; } diff --git a/app/Services/WechatBot/KafkaConsumerService.php b/app/Services/WechatBot/KafkaConsumerService.php index 2a2d054..6c03ebb 100644 --- a/app/Services/WechatBot/KafkaConsumerService.php +++ b/app/Services/WechatBot/KafkaConsumerService.php @@ -33,16 +33,32 @@ class KafkaConsumerService protected string $groupId = ''; /** - * 构造函数 — 从 SysParam 获取配置 + * 构造函数 */ public function __construct() { - $param = SysParam::where('alias', 'wechat_bot_config')->first(); - if ($param && ! empty($param->body)) { - $config = json_decode($param->body, true); - $this->brokers = $config['kafka']['brokers'] ?? ''; - $this->topic = $config['kafka']['topic'] ?? ''; - $this->groupId = $config['kafka']['group_id'] ?? 'chatroom_wechat_bot'; + // 延迟加载配置 + } + + /** + * 加载 Kafka 配置 + */ + protected function loadConfig(): void + { + if (! empty($this->brokers)) { + return; // 已经加载过 + } + + try { + $param = SysParam::where('alias', 'wechat_bot_config')->first(); + if ($param && ! empty($param->body)) { + $config = json_decode($param->body, true); + $this->brokers = $config['kafka']['brokers'] ?? ''; + $this->topic = $config['kafka']['topic'] ?? ''; + $this->groupId = $config['kafka']['group_id'] ?? 'chatroom_wechat_bot'; + } + } catch (\Throwable $e) { + Log::warning('加载 Kafka 配置失败', ['error' => $e->getMessage()]); } } @@ -51,6 +67,8 @@ class KafkaConsumerService */ public function createConsumer(): ?Consumer { + $this->loadConfig(); + if (empty($this->brokers) || empty($this->topic)) { Log::warning('WechatBot Kafka: brokers or topic is empty. Consumer not started.'); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..744f580 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -24,21 +24,12 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'username' => fake()->unique()->name(), 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'sex' => 1, + 'user_level' => 1, ]; } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } } diff --git a/database/migrations/2026_02_27_074855_create_shop_tables.php b/database/migrations/2026_02_27_074855_create_shop_tables.php index 948f6db..4281905 100644 --- a/database/migrations/2026_02_27_074855_create_shop_tables.php +++ b/database/migrations/2026_02_27_074855_create_shop_tables.php @@ -48,7 +48,7 @@ return new class extends Migration ['name' => '🌧️ 下雨周卡', 'slug' => 'week_rain', 'description' => '7天内进房间自动下雨', 'icon' => '🌧️', 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 6, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ['name' => '⚡ 雷电周卡', 'slug' => 'week_lightning', 'description' => '7天内进房间自动雷电', 'icon' => '⚡', 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 7, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ['name' => '❄️ 下雪周卡', 'slug' => 'week_snow', 'description' => '7天内进房间自动下雪', 'icon' => '❄️', 'price' => 8888, 'type' => 'duration', 'duration_days' => 7, 'sort_order' => 8, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], - ['name' => '✏️ 改名卡', 'slug' => 'rename_card', 'description' => '修改一次用户名(受黑名单限制)', 'icon' => '✏️', 'price' => 5000, 'type' => 'rename', 'duration_days' => null, 'sort_order' => 9, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], + ['name' => '✏️ 改名卡', 'slug' => 'rename_card', 'description' => '修改一次用户名(受黑名单限制)', 'icon' => '✏️', 'price' => 5000, 'type' => 'one_time', 'duration_days' => null, 'sort_order' => 9, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()], ]); // ── 购买记录表 ──────────────────────────────────────────────── diff --git a/database/migrations/2026_02_28_141645_create_feedback_tables.php b/database/migrations/2026_02_28_141645_create_feedback_tables.php index 0b9f3b7..953cce7 100644 --- a/database/migrations/2026_02_28_141645_create_feedback_tables.php +++ b/database/migrations/2026_02_28_141645_create_feedback_tables.php @@ -67,7 +67,7 @@ return new class extends Migration // 联合唯一索引:每个用户每条反馈只能赞同一次 $table->unique(['feedback_id', 'user_id'], 'uq_vote'); - $table->index('feedback_id', 'idx_feedback_id'); + $table->index('feedback_id'); }); // ——— 表3:补充评论 ——— @@ -83,7 +83,7 @@ return new class extends Migration $table->boolean('is_admin')->default(false)->comment('是否为 id=1 管理员的官方回复'); $table->timestamps(); - $table->index('feedback_id', 'idx_feedback_id'); + $table->index('feedback_id'); }); } diff --git a/phpunit.xml b/phpunit.xml index d703241..6c94aec 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,8 +23,8 @@ - - + + diff --git a/tests/Feature/AuthControllerTest.php b/tests/Feature/AuthControllerTest.php new file mode 100644 index 0000000..0bb8a55 --- /dev/null +++ b/tests/Feature/AuthControllerTest.php @@ -0,0 +1,184 @@ + 'superlevel'], + ['body' => '100'] + ); + } + + public function test_can_register_new_user() + { + Redis::shouldReceive('sismember')->with('banned_ips', '127.0.0.1')->andReturn(false); + + $response = $this->postJson('/login', [ + 'username' => 'newuser', + 'password' => 'secret123', + 'captcha' => '1234', + 'bSex' => '1', + ]); + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + + $this->assertDatabaseHas('users', [ + 'username' => 'newuser', + 'user_level' => 1, + 'sex' => 1, + ]); + + $this->assertAuthenticated(); + } + + public function test_cannot_register_with_blacklisted_username() + { + Redis::shouldReceive('sismember')->with('banned_ips', '127.0.0.1')->andReturn(false); + + UsernameBlacklist::create([ + 'username' => 'admin', + 'type' => 'permanent', + ]); + + $response = $this->postJson('/login', [ + 'username' => 'admin', + 'password' => 'secret123', + 'captcha' => '1234', + ]); + + $response->assertStatus(422) + ->assertJsonPath('status', 'error'); + + $this->assertDatabaseMissing('users', [ + 'username' => 'admin', + ]); + + $this->assertGuest(); + } + + public function test_can_login_existing_user() + { + Redis::shouldReceive('sismember')->with('banned_ips', '127.0.0.1')->andReturn(false); + + $user = User::factory()->create([ + 'username' => 'testuser', + 'password' => Hash::make('password123'), + ]); + + $response = $this->postJson('/login', [ + 'username' => 'testuser', + 'password' => 'password123', + 'captcha' => '1234', + ]); + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + + $this->assertAuthenticatedAs($user); + } + + public function test_login_md5_user_upgrades_to_bcrypt() + { + Redis::shouldReceive('sismember')->with('banned_ips', '127.0.0.1')->andReturn(false); + + $password = 'oldsecret'; + $user = User::factory()->create([ + 'username' => 'olduser', + 'password' => 'temp', + ]); + + \Illuminate\Support\Facades\DB::table('users') + ->where('id', $user->id) + ->update(['password' => md5($password)]); + + $response = $this->postJson('/login', [ + 'username' => 'olduser', + 'password' => $password, + 'captcha' => '1234', + ]); + + if ($response->status() !== 200) { + dd($response->json()); + } + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + + $user->refresh(); + $this->assertTrue(Hash::check($password, $user->password)); + $this->assertAuthenticatedAs($user); + } + + public function test_banned_user_cannot_login() + { + $user = User::factory()->create([ + 'username' => 'banneduser', + 'password' => Hash::make('secret123'), + 'user_level' => -1, // banned + ]); + + $response = $this->postJson('/login', [ + 'username' => 'banneduser', + 'password' => 'secret123', + 'captcha' => '1234', + ]); + + $response->assertStatus(403) + ->assertJsonPath('status', 'error'); + + $this->assertGuest(); + } + + public function test_banned_ip_cannot_login() + { + Redis::shouldReceive('sismember')->with('banned_ips', '127.0.0.1')->andReturn(true); + + $user = User::factory()->create([ + 'username' => 'normaluser', + 'password' => Hash::make('secret'), + ]); + + $response = $this->postJson('/login', [ + 'username' => 'normaluser', + 'password' => 'secret', + 'captcha' => '1234', + ]); + + $response->assertStatus(403); + $this->assertGuest(); + } + + public function test_can_logout() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $response->assertRedirect('/'); + $this->assertGuest(); + } +} diff --git a/tests/Feature/BaccaratControllerTest.php b/tests/Feature/BaccaratControllerTest.php new file mode 100644 index 0000000..8c50bb6 --- /dev/null +++ b/tests/Feature/BaccaratControllerTest.php @@ -0,0 +1,200 @@ + 'baccarat'], + [ + 'name' => 'Baccarat', + 'icon' => 'baccarat', + 'description' => 'Baccarat Game', + 'enabled' => true, + 'params' => [ + 'min_bet' => 100, + 'max_bet' => 50000, + ], + ] + ); + } + + public function test_can_get_current_round() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $round = BaccaratRound::forceCreate([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + $response = $this->actingAs($user)->getJson(route('baccarat.current')); + + $response->assertStatus(200); + $response->assertJsonStructure(['round' => ['id', 'status', 'bet_closes_at']]); + $this->assertEquals($round->id, $response->json('round.id')); + } + + public function test_can_bet() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 200]); + + $round = BaccaratRound::forceCreate([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('baccarat.bet'), [ + 'round_id' => $round->id, + 'bet_type' => 'big', + 'amount' => 100, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + $this->assertEquals(100, $user->fresh()->jjb); + $this->assertDatabaseHas('baccarat_bets', [ + 'round_id' => $round->id, + 'user_id' => $user->id, + 'bet_type' => 'big', + 'amount' => 100, + ]); + + Event::assertDispatched(\App\Events\BaccaratPoolUpdated::class); + } + + public function test_cannot_bet_out_of_range() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 200]); + + $round = BaccaratRound::forceCreate([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('baccarat.bet'), [ + 'round_id' => $round->id, + 'bet_type' => 'big', + 'amount' => 50, // Less than min_bet + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_cannot_bet_twice_in_same_round() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 200]); + + $round = BaccaratRound::forceCreate([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + BaccaratBet::forceCreate([ + 'round_id' => $round->id, + 'user_id' => $user->id, + 'bet_type' => 'big', + 'amount' => 100, + 'status' => 'pending', + ]); + + $response = $this->actingAs($user)->postJson(route('baccarat.bet'), [ + 'round_id' => $round->id, + 'bet_type' => 'small', + 'amount' => 100, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_can_get_history() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + BaccaratRound::forceCreate([ + 'status' => 'settled', + 'bet_opens_at' => now()->subMinutes(2), + 'bet_closes_at' => now()->subMinutes(1), + 'settled_at' => now(), + 'dice1' => 1, + 'dice2' => 2, + 'dice3' => 3, + 'total_points' => 6, + 'result' => 'small', + 'total_bet_big' => 0, + 'total_bet_small' => 0, + 'total_bet_triple' => 0, + 'bet_count' => 0, + 'bet_count_big' => 0, + 'bet_count_small' => 0, + 'bet_count_triple' => 0, + 'total_payout' => 0, + ]); + + $response = $this->actingAs($user)->getJson(route('baccarat.history')); + + $response->assertStatus(200); + $response->assertJsonStructure(['history']); + $this->assertCount(1, $response->json('history')); + } +} diff --git a/tests/Feature/BankControllerTest.php b/tests/Feature/BankControllerTest.php new file mode 100644 index 0000000..561ed66 --- /dev/null +++ b/tests/Feature/BankControllerTest.php @@ -0,0 +1,154 @@ +create([ + 'jjb' => 1000, + 'bank_jjb' => 5000, + ]); + + BankLog::create([ + 'user_id' => $user->id, + 'type' => 'deposit', + 'amount' => 500, + 'balance_after' => 5000, + ]); + + $response = $this->actingAs($user)->getJson(route('bank.info')); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'jjb' => 1000, + 'bank_jjb' => 5000, + ]); + $response->assertJsonCount(1, 'logs'); + } + + public function test_deposit_transfers_jjb_to_bank() + { + $user = User::factory()->create([ + 'jjb' => 1000, + 'bank_jjb' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('bank.deposit'), [ + 'amount' => 500, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'jjb' => 500, + 'bank_jjb' => 500, + ]); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'jjb' => 500, + 'bank_jjb' => 500, + ]); + + $this->assertDatabaseHas('bank_logs', [ + 'user_id' => $user->id, + 'type' => 'deposit', + 'amount' => 500, + 'balance_after' => 500, + ]); + } + + public function test_deposit_fails_if_insufficient_funds() + { + $user = User::factory()->create([ + 'jjb' => 100, + 'bank_jjb' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('bank.deposit'), [ + 'amount' => 500, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'error', + ]); + + $this->assertDatabaseMissing('bank_logs', [ + 'user_id' => $user->id, + ]); + } + + public function test_withdraw_transfers_bank_to_jjb() + { + $user = User::factory()->create([ + 'jjb' => 0, + 'bank_jjb' => 1000, + ]); + + $response = $this->actingAs($user)->postJson(route('bank.withdraw'), [ + 'amount' => 500, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'jjb' => 500, + 'bank_jjb' => 500, + ]); + + $this->assertDatabaseHas('bank_logs', [ + 'user_id' => $user->id, + 'type' => 'withdraw', + 'amount' => 500, + 'balance_after' => 500, + ]); + } + + public function test_withdraw_fails_if_insufficient_funds() + { + $user = User::factory()->create([ + 'jjb' => 0, + 'bank_jjb' => 100, + ]); + + $response = $this->actingAs($user)->postJson(route('bank.withdraw'), [ + 'amount' => 500, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'error', + ]); + } + + public function test_ranking_returns_paginated_users_ordered_by_bank_jjb() + { + $user = User::factory()->create(); // To act as + + User::factory()->create(['bank_jjb' => 1000, 'username' => 'Rich']); + User::factory()->create(['bank_jjb' => 500, 'username' => 'Poorer']); + + $response = $this->actingAs($user)->getJson(route('bank.ranking')); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + ]); + + $ranking = $response->json('ranking'); + $this->assertCount(2, $ranking); + $this->assertEquals('Rich', $ranking[0]['username']); + $this->assertEquals('Poorer', $ranking[1]['username']); + } +} diff --git a/tests/Feature/ChatBotControllerTest.php b/tests/Feature/ChatBotControllerTest.php new file mode 100644 index 0000000..4697a83 --- /dev/null +++ b/tests/Feature/ChatBotControllerTest.php @@ -0,0 +1,125 @@ +create(); + + $response = $this->actingAs($user)->postJson(route('chatbot.chat'), [ + 'message' => 'Hello', + 'room_id' => 1, + ]); + + $response->assertStatus(403); + $response->assertJson(['status' => 'error']); + } + + public function test_chatbot_can_reply() + { + $user = User::factory()->create(); + + Sysparam::updateOrCreate(['alias' => 'chatbot_enabled'], ['body' => '1']); + + User::factory()->create([ + 'username' => 'AI小班长', + 'exp_num' => 0, + 'jjb' => 0, + ]); + + // Mock the AiChatService + $this->mock(AiChatService::class, function (MockInterface $mock) { + $mock->shouldReceive('chat') + ->once() + ->andReturn([ + 'reply' => 'Hello from AI', + 'provider' => 'test_provider', + 'model' => 'test_model', + ]); + }); + + $response = $this->actingAs($user)->postJson(route('chatbot.chat'), [ + 'message' => 'Hello', + 'room_id' => 1, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'reply' => 'Hello from AI', + 'provider' => 'test_provider', + ]); + } + + public function test_chatbot_can_give_gold() + { + $user = User::factory()->create(['jjb' => 0]); + + Sysparam::updateOrCreate(['alias' => 'chatbot_enabled'], ['body' => '1']); + Sysparam::updateOrCreate(['alias' => 'chatbot_max_daily_rewards'], ['body' => '1']); + Sysparam::updateOrCreate(['alias' => 'chatbot_max_gold'], ['body' => '500']); + + User::factory()->create([ + 'username' => 'AI小班长', + 'exp_num' => 0, + 'jjb' => 1000, // Ensure AI bot has enough gold + ]); + + // Mock the AiChatService + $this->mock(AiChatService::class, function (MockInterface $mock) { + $mock->shouldReceive('chat') + ->once() + ->andReturn([ + 'reply' => 'Here is some gold! [ACTION:GIVE_GOLD]', + 'provider' => 'test_provider', + 'model' => 'test_model', + ]); + }); + + $response = $this->actingAs($user)->postJson(route('chatbot.chat'), [ + 'message' => 'Give me gold', + 'room_id' => 1, + ]); + + $response->assertStatus(200); + + // User should have received gold + $user->refresh(); + $this->assertGreaterThan(0, $user->jjb); + $this->assertLessThanOrEqual(500, $user->jjb); + } + + public function test_clear_context() + { + $user = User::factory()->create(); + + $this->mock(AiChatService::class, function (MockInterface $mock) use ($user) { + $mock->shouldReceive('clearContext') + ->once() + ->with($user->id); + }); + + $response = $this->actingAs($user)->postJson(route('chatbot.clear')); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + } +} diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php new file mode 100644 index 0000000..c59e81e --- /dev/null +++ b/tests/Feature/ChatControllerTest.php @@ -0,0 +1,150 @@ + 'testroom']); + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('chat.room', $room->id)); + + $response->assertStatus(200); + $response->assertViewIs('chat.frame'); + + // Assert user was added to room in redis + $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); + } + + public function test_can_send_message() + { + $room = Room::create(['room_name' => 'test_send']); + $user = User::factory()->create(); + + // 进房 + $this->actingAs($user)->get(route('chat.room', $room->id)); + + $response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [ + 'to_user' => '大家', + 'content' => '测试消息', + 'is_secret' => false, + 'font_color' => '#000000', + 'action' => 'say', + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + // 查看 Redis 里的消息记录 + $messages = Redis::lrange("room:{$room->id}:messages", 0, -1); + $this->assertNotEmpty($messages); + + $found = false; + foreach ($messages as $msgJson) { + $msg = json_decode($msgJson, true); + if ($msg['from_user'] === $user->username && $msg['content'] === '测试消息') { + $found = true; + break; + } + } + $this->assertTrue($found, 'Message not found in Redis'); + } + + public function test_can_trigger_heartbeat() + { + $room = Room::create(['room_name' => 'test_hb']); + $user = User::factory()->create(['exp_num' => 0]); + + $response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id)); + + $response->assertStatus(200); + $response->assertJsonFragment(['status' => 'success']); + + $user->refresh(); + $this->assertGreaterThanOrEqual(0, $user->exp_num); // Might be 1 depending on sysparam + } + + public function test_can_leave_room() + { + $room = Room::create(['room_name' => 'test_leave']); + $user = User::factory()->create(); + + // 进房 + $this->actingAs($user)->get(route('chat.room', $room->id)); + $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); + + // 显式退房 + $response = $this->actingAs($user)->postJson(route('chat.leave', $room->id).'?explicit=1'); + + $response->assertStatus(200); + + // 缓存中被移除 + $this->assertEquals(0, Redis::hexists("room:{$room->id}:users", $user->username)); + } + + public function test_can_get_rooms_online_status() + { + $user = User::factory()->create(); + $room1 = Room::create(['room_name' => 'room1']); + $room2 = Room::create(['room_name' => 'room2']); + + $this->actingAs($user)->get(route('chat.room', $room1->id)); + + $response = $this->actingAs($user)->getJson(route('chat.rooms-online-status')); + + $response->assertStatus(200); + + // Assert room1 has 1 online, room2 has 0 + $response->assertJsonFragment([ + 'id' => $room1->id, + 'online' => 1, + ]); + $response->assertJsonFragment([ + 'id' => $room2->id, + 'online' => 0, + ]); + } + + public function test_can_set_announcement() + { + $user = User::factory()->create(['user_level' => 100]); // superadmin + $room = Room::create(['room_name' => 'test_ann', 'room_owner' => 'someone']); + + $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [ + 'announcement' => 'This is a new test announcement', + ]); + + $response->assertStatus(200); + + $room->refresh(); + $this->assertStringContainsString('This is a new test announcement', $room->announcement); + } + + public function test_cannot_set_announcement_without_permission() + { + $user = User::factory()->create(['user_level' => 0]); + $room = Room::create(['room_name' => 'test_ann2', 'room_owner' => 'someone']); + + $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [ + 'announcement' => 'This is a new test announcement', + ]); + + $response->assertStatus(403); + } +} diff --git a/tests/Feature/DutyHallControllerTest.php b/tests/Feature/DutyHallControllerTest.php new file mode 100644 index 0000000..9a69c3b --- /dev/null +++ b/tests/Feature/DutyHallControllerTest.php @@ -0,0 +1,84 @@ +create(); + + $department = Department::create(['name' => 'Support', 'rank' => 1]); + $position = Position::create(['name' => 'Helper', 'department_id' => $department->id, 'rank' => 1, 'type' => 'temp']); + + UserPosition::create([ + 'user_id' => $user->id, + 'position_id' => $position->id, + 'status' => 'active', + 'appointed_at' => now(), + ]); + + $response = $this->actingAs($user)->get(route('duty-hall.index', ['tab' => 'roster'])); + + $response->assertStatus(200); + $response->assertViewIs('duty-hall.index'); + $response->assertViewHas('tab', 'roster'); + + $currentStaff = $response->viewData('currentStaff'); + $this->assertNotNull($currentStaff); + $this->assertEquals(1, $currentStaff->count()); + $this->assertEquals('Support', $currentStaff->first()->name); + } + + public function test_can_view_duty_hall_leaderboard_tabs() + { + $user = User::factory()->create(); + + PositionDutyLog::create([ + 'user_id' => $user->id, + 'position_id' => 1, + 'login_at' => now()->subMinutes(30), + 'logout_at' => now(), + 'duration_seconds' => 1800, + ]); + + PositionAuthorityLog::create([ + 'user_id' => $user->id, // operator + 'target_user_id' => $user->id, // target + 'action_type' => 'reward', + 'amount' => 500, + ]); + + $tabs = ['day', 'week', 'month', 'all']; + + foreach ($tabs as $tab) { + $response = $this->actingAs($user)->get(route('duty-hall.index', ['tab' => $tab])); + + $response->assertStatus(200); + $response->assertViewIs('duty-hall.index'); + $response->assertViewHas('tab', $tab); + + $leaderboard = $response->viewData('leaderboard'); + $this->assertNotNull($leaderboard); + + if ($leaderboard->count() > 0) { + $row = $leaderboard->first(); + $this->assertEquals($user->id, $row->user_id); + $this->assertEquals(1800, $row->total_seconds); + $this->assertEquals(1, $row->reward_count); + $this->assertEquals(500, $row->reward_total); + } + } + } +} diff --git a/tests/Feature/EarnControllerTest.php b/tests/Feature/EarnControllerTest.php new file mode 100644 index 0000000..7a2b157 --- /dev/null +++ b/tests/Feature/EarnControllerTest.php @@ -0,0 +1,78 @@ +create([ + 'jjb' => 0, + 'exp_num' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('earn.video_reward'), [ + 'room_id' => 1, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'success' => true, + ]); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'jjb' => 5000, // Reward is hardcoded to 5000 + 'exp_num' => 500, // Exp is hardcoded to 500 + ]); + + // Cooldown should be set + $this->assertTrue((bool) Redis::exists("earn_video:cooldown:{$user->id}")); + } + + public function test_cannot_claim_during_cooldown() + { + $user = User::factory()->create(); + + // First claim + $this->actingAs($user)->postJson(route('earn.video_reward')); + + // Second claim immediately should fail due to cooldown + $response = $this->actingAs($user)->postJson(route('earn.video_reward')); + + $response->assertStatus(200); + $response->assertJson([ + 'success' => false, + 'message' => '操作过快,请稍后再试。', + ]); + } + + public function test_cannot_exceed_daily_limit() + { + $user = User::factory()->create(); + $dateKey = now()->format('Y-m-d'); + + // Manually set daily count to max limit (3) + Redis::set("earn_video:count:{$user->id}:{$dateKey}", 3); + + $response = $this->actingAs($user)->postJson(route('earn.video_reward')); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'success' => false, + ]); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8364a84..22ef0c5 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -2,11 +2,13 @@ namespace Tests\Feature; -// use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ExampleTest extends TestCase { + use RefreshDatabase; + /** * A basic test example. */ diff --git a/tests/Feature/FishingControllerTest.php b/tests/Feature/FishingControllerTest.php new file mode 100644 index 0000000..08b471a --- /dev/null +++ b/tests/Feature/FishingControllerTest.php @@ -0,0 +1,125 @@ + 'fishing'], + [ + 'name' => 'Fishing', + 'icon' => 'fish', + 'description' => 'Fishing Game', + 'enabled' => true, + 'params' => [ + 'fishing_cost' => 5, + 'fishing_cooldown' => 300, + ], + ] + ); + } + + public function test_can_cast_rod() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10]); + + $response = $this->actingAs($user)->postJson(route('fishing.cast', ['id' => 1])); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'cost' => 5, + ]); + + $this->assertEquals(5, $user->fresh()->jjb); + } + + public function test_cannot_cast_when_on_cooldown() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10]); + Redis::setex("fishing:cd:{$user->id}", 100, time()); + + $response = $this->actingAs($user)->postJson(route('fishing.cast', ['id' => 1])); + + $response->assertStatus(429); + $response->assertJson([ + 'status' => 'error', + ]); + } + + public function test_cannot_cast_without_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 2]); + + $response = $this->actingAs($user)->postJson(route('fishing.cast', ['id' => 1])); + + $response->assertStatus(422); + $response->assertJson([ + 'status' => 'error', + ]); + } + + public function test_can_reel_after_waiting() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10]); + + $token = 'test-token'; + $waitTime = 0; // Set to 0 so we can test immediately + + Redis::set("fishing:token:{$user->id}", json_encode([ + 'token' => $token, + 'cast_at' => time() - 1, // Simulate past + 'wait_time' => 0, + ])); + + $response = $this->actingAs($user)->postJson(route('fishing.reel', ['id' => 1]), [ + 'token' => $token, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + ]); + + // Cooldown should be set + $this->assertTrue((bool) Redis::exists("fishing:cd:{$user->id}")); + } + + public function test_cannot_reel_with_invalid_token() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10]); + + Redis::set("fishing:token:{$user->id}", json_encode([ + 'token' => 'valid-token', + 'cast_at' => time(), + 'wait_time' => 0, + ])); + + $response = $this->actingAs($user)->postJson(route('fishing.reel', ['id' => 1]), [ + 'token' => 'invalid-token', + ]); + + $response->assertStatus(422); + $response->assertJson([ + 'status' => 'error', + ]); + } +} diff --git a/tests/Feature/FortuneTellingControllerTest.php b/tests/Feature/FortuneTellingControllerTest.php new file mode 100644 index 0000000..90de760 --- /dev/null +++ b/tests/Feature/FortuneTellingControllerTest.php @@ -0,0 +1,142 @@ + 'fortune_telling'], + [ + 'name' => 'Fortune Telling', + 'icon' => 'fortune', + 'description' => 'Fortune Telling Game', + 'enabled' => true, + 'params' => [ + 'free_count_per_day' => 1, + 'extra_cost' => 500, + ], + ] + ); + } + + public function test_can_get_today_status() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->getJson(route('fortune.today')); + + $response->assertStatus(200); + $response->assertJson([ + 'enabled' => true, + 'today_count' => 0, + 'free_count' => 1, + 'free_used' => 0, + 'has_free_left' => true, + 'extra_cost' => 500, + ]); + $response->assertJsonStructure(['latest']); + } + + public function test_can_tell_free() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 0]); // Note: 0 jjb needed for free + + $response = $this->actingAs($user)->postJson(route('fortune.tell')); + + $response->assertStatus(200); + $response->assertJson(['ok' => true, 'is_free' => true]); + + $this->assertDatabaseHas('fortune_logs', [ + 'user_id' => $user->id, + 'is_free' => 1, + ]); + } + + public function test_cannot_tell_paid_without_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 0]); + + // Consume free tell + FortuneLog::create([ + 'user_id' => $user->id, + 'grade' => 'great_luck', + 'text' => 'Test', + 'buff_desc' => 'Test', + 'is_free' => true, + 'cost' => 0, + 'fortune_date' => today(), + ]); + + $response = $this->actingAs($user)->postJson(route('fortune.tell')); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_can_tell_paid_with_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 500]); + + // Consume free tell + FortuneLog::create([ + 'user_id' => $user->id, + 'grade' => 'great_luck', + 'text' => 'Test', + 'buff_desc' => 'Test', + 'is_free' => true, + 'cost' => 0, + 'fortune_date' => today(), + ]); + + $response = $this->actingAs($user)->postJson(route('fortune.tell')); + + $response->assertStatus(200); + $response->assertJson(['ok' => true, 'is_free' => false]); + + $this->assertDatabaseHas('fortune_logs', [ + 'user_id' => $user->id, + 'is_free' => 0, + 'cost' => 500, + ]); + + $this->assertEquals(0, $user->fresh()->jjb); + } + + public function test_can_get_history() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + FortuneLog::create([ + 'user_id' => $user->id, + 'grade' => 'great_luck', + 'text' => 'Test', + 'buff_desc' => 'Test', + 'is_free' => true, + 'cost' => 0, + 'fortune_date' => today(), + ]); + + $response = $this->actingAs($user)->getJson(route('fortune.history')); + + $response->assertStatus(200); + $response->assertJsonStructure(['history']); + $this->assertCount(1, $response->json('history')); + } +} diff --git a/tests/Feature/FriendControllerTest.php b/tests/Feature/FriendControllerTest.php new file mode 100644 index 0000000..3a10e2e --- /dev/null +++ b/tests/Feature/FriendControllerTest.php @@ -0,0 +1,169 @@ +create(); + $target = User::factory()->create(); + + // 此时不是好友 + $response = $this->actingAs($me)->getJson(route('friend.status', $target->username)); + $response->assertStatus(200); + $response->assertJson([ + 'is_friend' => false, + 'mutual' => false, + ]); + + // 我加了对方 + FriendRequest::create(['who' => $me->username, 'towho' => $target->username, 'sub_time' => now()]); + + $response = $this->actingAs($me)->getJson(route('friend.status', $target->username)); + $response->assertStatus(200); + $response->assertJson([ + 'is_friend' => true, + 'mutual' => false, + ]); + + // 对方也加了我 + FriendRequest::create(['who' => $target->username, 'towho' => $me->username, 'sub_time' => now()]); + + $response = $this->actingAs($me)->getJson(route('friend.status', $target->username)); + $response->assertStatus(200); + $response->assertJson([ + 'is_friend' => true, + 'mutual' => true, + ]); + } + + public function test_cannot_add_self_as_friend() + { + $me = User::factory()->create(); + + $response = $this->actingAs($me)->postJson(route('friend.add', $me->username)); + + $response->assertStatus(422); + $response->assertJsonFragment(['message' => '不能将自己加为好友']); + } + + public function test_cannot_add_nonexistent_user() + { + $me = User::factory()->create(); + + $response = $this->actingAs($me)->postJson(route('friend.add', 'nonexistent_foo')); + + $response->assertStatus(404); + $response->assertJsonFragment(['message' => '用户不存在']); + } + + public function test_can_add_friend() + { + $me = User::factory()->create(); + $target = User::factory()->create(); + + $response = $this->actingAs($me)->postJson(route('friend.add', $target->username)); + + $response->assertStatus(200); + $response->assertJsonFragment(['status' => 'success']); + + $this->assertDatabaseHas('friend_requests', [ + 'who' => $me->username, + 'towho' => $target->username, + ]); + } + + public function test_cannot_add_same_friend_twice() + { + $me = User::factory()->create(); + $target = User::factory()->create(); + + FriendRequest::create(['who' => $me->username, 'towho' => $target->username, 'sub_time' => now()]); + + $response = $this->actingAs($me)->postJson(route('friend.add', $target->username)); + + $response->assertStatus(422); + $response->assertJsonFragment(['message' => '已是好友,无需重复添加']); + + // 确保只有1条记录 + $this->assertEquals(1, FriendRequest::where('who', $me->username)->where('towho', $target->username)->count()); + } + + public function test_can_remove_friend() + { + $me = User::factory()->create(); + $target = User::factory()->create(); + + FriendRequest::create(['who' => $me->username, 'towho' => $target->username, 'sub_time' => now()]); + + $response = $this->actingAs($me)->deleteJson(route('friend.remove', $target->username)); + + $response->assertStatus(200); + $response->assertJsonFragment(['status' => 'success']); + + $this->assertDatabaseMissing('friend_requests', [ + 'who' => $me->username, + 'towho' => $target->username, + ]); + } + + public function test_cannot_remove_non_friend() + { + $me = User::factory()->create(); + $target = User::factory()->create(); + + $response = $this->actingAs($me)->deleteJson(route('friend.remove', $target->username)); + + $response->assertStatus(404); + $response->assertJsonFragment(['message' => '好友关系不存在']); + } + + public function test_can_view_friend_list() + { + $me = User::factory()->create(); + + $myFriend = User::factory()->create(); + $someoneWhoAddedMe = User::factory()->create(); + $mutualFriend = User::factory()->create(); + + // 我加了他 + FriendRequest::create(['who' => $me->username, 'towho' => $myFriend->username, 'sub_time' => now()]); + + // 他加了我 (Pending) + FriendRequest::create(['who' => $someoneWhoAddedMe->username, 'towho' => $me->username, 'sub_time' => now()]); + + // 互相好友 + FriendRequest::create(['who' => $me->username, 'towho' => $mutualFriend->username, 'sub_time' => now()]); + FriendRequest::create(['who' => $mutualFriend->username, 'towho' => $me->username, 'sub_time' => now()]); + + $response = $this->actingAs($me)->getJson(route('friend.index')); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'status', + 'friends' => [ + '*' => ['username', 'headface', 'user_level', 'sex', 'mutual', 'sub_time', 'is_online'], + ], + 'pending' => [ + '*' => ['username', 'headface', 'user_level', 'sex', 'added_at', 'is_online'], + ], + ]); + + $responseData = $response->json(); + + $this->assertCount(2, $responseData['friends']); + $this->assertCount(1, $responseData['pending']); + + // 验证 pending 列表 + $this->assertEquals($someoneWhoAddedMe->username, $responseData['pending'][0]['username']); + } +} diff --git a/tests/Feature/GomokuControllerTest.php b/tests/Feature/GomokuControllerTest.php new file mode 100644 index 0000000..657746c --- /dev/null +++ b/tests/Feature/GomokuControllerTest.php @@ -0,0 +1,176 @@ + 'gomoku'], + [ + 'name' => 'Gomoku', + 'icon' => 'gomoku', + 'description' => 'Gomoku Game', + 'enabled' => true, + 'params' => [ + 'invite_timeout' => 60, + 'pvp_reward' => 80, + 'pve_easy_fee' => 0, + 'pve_normal_fee' => 10, + 'pve_hard_fee' => 30, + 'pve_expert_fee' => 80, + ], + ] + ); + } + + private function createRoom(int $ownerId) + { + return Room::forceCreate([ + 'id' => 100, // Safe room id + 'room_name' => 'test', + 'room_auto' => 'open', + 'room_owner' => 'owner', + 'room_des' => 'test description', + 'announcement' => 'welcome', + 'room_top' => '', + 'room_title' => 'test title', + 'room_keep' => 0, + 'room_time' => now(), + 'room_tt' => 0, + 'room_html' => 0, + 'room_exp' => 0, + 'build_time' => now(), + 'permit_level' => 0, + 'door_open' => 1, + 'ooooo' => 0, + 'visit_num' => 0, + ]); + } + + public function test_can_create_pve_game() + { + /** @var \App\Models\User $owner */ + $owner = User::factory()->create(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $room = $this->createRoom($owner->id); + + $response = $this->actingAs($user)->postJson(route('gomoku.create'), [ + 'mode' => 'pve', + 'room_id' => $room->id, + 'ai_level' => 1, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + $this->assertDatabaseHas('gomoku_games', [ + 'mode' => 'pve', + 'player_black_id' => $user->id, + ]); + } + + public function test_can_create_pvp_game() + { + Event::fake(); + + /** @var \App\Models\User $owner */ + $owner = User::factory()->create(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $room = $this->createRoom($owner->id); + + $response = $this->actingAs($user)->postJson(route('gomoku.create'), [ + 'mode' => 'pvp', + 'room_id' => $room->id, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + $this->assertDatabaseHas('gomoku_games', [ + 'mode' => 'pvp', + 'status' => 'waiting', + 'player_black_id' => $user->id, + ]); + } + + public function test_user_can_join_pvp() + { + /** @var \App\Models\User $owner */ + $owner = User::factory()->create(); + + /** @var \App\Models\User $user1 */ + $user1 = User::factory()->create(); + + /** @var \App\Models\User $user2 */ + $user2 = User::factory()->create(); + + $room = $this->createRoom($owner->id); + + $game = GomokuGame::create([ + 'mode' => 'pvp', + 'room_id' => $room->id, + 'player_black_id' => $user1->id, + 'status' => 'waiting', + 'board' => [], + 'current_turn' => 1, + 'entry_fee' => 0, + 'invite_expires_at' => now()->addMinutes(1), + ]); + + $response = $this->actingAs($user2)->postJson(route('gomoku.join', $game)); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + $this->assertEquals($user2->id, $game->fresh()->player_white_id); + $this->assertEquals('playing', $game->fresh()->status); + } + + public function test_user_can_get_active_game() + { + /** @var \App\Models\User $owner */ + $owner = User::factory()->create(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $room = $this->createRoom($owner->id); + + $game = GomokuGame::create([ + 'mode' => 'pve', + 'room_id' => $room->id, + 'player_black_id' => $user->id, + 'ai_level' => 1, + 'status' => 'playing', + 'board' => [], + 'current_turn' => 1, + 'entry_fee' => 0, + ]); + + $response = $this->actingAs($user)->getJson(route('gomoku.active')); + + $response->assertStatus(200); + $response->assertJson([ + 'ok' => true, + 'has_active' => true, + 'game_id' => $game->id, + ]); + } +} diff --git a/tests/Feature/GuestbookControllerTest.php b/tests/Feature/GuestbookControllerTest.php new file mode 100644 index 0000000..78ebfb0 --- /dev/null +++ b/tests/Feature/GuestbookControllerTest.php @@ -0,0 +1,173 @@ +create(); + $otherUser = User::factory()->create(); + + // Public message + Guestbook::create([ + 'who' => $otherUser->username, + 'towho' => null, + 'secret' => 0, + 'text_title' => 'Public Title', + 'text_body' => 'Public message body', + 'ip' => '127.0.0.1', + 'post_time' => now(), + ]); + + // Secret message to someone else + Guestbook::create([ + 'who' => $otherUser->username, + 'towho' => 'anotheruser', + 'secret' => 1, + 'text_title' => 'Secret Title', + 'text_body' => 'Secret message body', + 'ip' => '127.0.0.1', + 'post_time' => now(), + ]); + + $response = $this->actingAs($user)->get(route('guestbook.index', ['tab' => 'public'])); + + $response->assertStatus(200); + $response->assertViewIs('guestbook.index'); + $response->assertSee('Public message body'); + $response->assertDontSee('Secret message body'); + } + + public function test_can_post_public_message() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post(route('guestbook.store'), [ + 'text_title' => 'Hello', + 'text_body' => 'World', + ]); + + $response->assertRedirect(); + + $this->assertDatabaseHas('guestbooks', [ + 'who' => $user->username, + 'towho' => null, + 'secret' => 0, + 'text_body' => 'World', + ]); + } + + public function test_can_post_secret_message_to_user() + { + $user = User::factory()->create(); + $targetUser = User::factory()->create(['username' => 'target']); + + $response = $this->actingAs($user)->post(route('guestbook.store'), [ + 'text_title' => 'Secret', + 'text_body' => 'Top secret', + 'towho' => 'target', + 'secret' => 1, + ]); + + $response->assertRedirect(); + + $this->assertDatabaseHas('guestbooks', [ + 'who' => $user->username, + 'towho' => 'target', + 'secret' => 1, + 'text_body' => 'Top secret', + ]); + } + + public function test_cannot_post_message_to_non_existent_user() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post(route('guestbook.store'), [ + 'text_title' => 'Secret', + 'text_body' => 'Top secret', + 'towho' => 'nonexistent', + 'secret' => 1, + ]); + + $response->assertRedirect(); + $response->assertSessionHas('error'); + + $this->assertDatabaseMissing('guestbooks', [ + 'who' => $user->username, + 'towho' => 'nonexistent', + ]); + } + + public function test_user_can_delete_own_message() + { + $user = User::factory()->create(); + + $message = Guestbook::create([ + 'who' => $user->username, + 'towho' => null, + 'secret' => 0, + 'text_title' => 'My Body', + 'text_body' => 'Delete me', + 'ip' => '127.0.0.1', + 'post_time' => now(), + ]); + + $response = $this->actingAs($user)->delete(route('guestbook.destroy', $message->id)); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + $this->assertDatabaseMissing('guestbooks', ['id' => $message->id]); + } + + public function test_user_cannot_delete_others_message() + { + $owner = User::factory()->create(); + $otherUser = User::factory()->create(['user_level' => 1]); // regular user + + $message = Guestbook::create([ + 'who' => $owner->username, + 'towho' => null, + 'secret' => 0, + 'text_title' => 'Their Body', + 'text_body' => 'Cant touch this', + 'ip' => '127.0.0.1', + 'post_time' => now(), + ]); + + $response = $this->actingAs($otherUser)->delete(route('guestbook.destroy', $message->id)); + + $response->assertStatus(403); + $this->assertDatabaseHas('guestbooks', ['id' => $message->id]); + } + + public function test_admin_can_delete_others_message() + { + $owner = User::factory()->create(); + $admin = User::factory()->create(['user_level' => 15]); + + $message = Guestbook::create([ + 'who' => $owner->username, + 'towho' => null, + 'secret' => 0, + 'text_title' => 'Their Body', + 'text_body' => 'Delete by admin', + 'ip' => '127.0.0.1', + 'post_time' => now(), + ]); + + $response = $this->actingAs($admin)->delete(route('guestbook.destroy', $message->id)); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + $this->assertDatabaseMissing('guestbooks', ['id' => $message->id]); + } +} diff --git a/tests/Feature/HolidayControllerTest.php b/tests/Feature/HolidayControllerTest.php new file mode 100644 index 0000000..55fb92b --- /dev/null +++ b/tests/Feature/HolidayControllerTest.php @@ -0,0 +1,139 @@ +create(); + $event = HolidayEvent::create([ + 'name' => 'Test Holiday', + 'status' => 'active', + 'expires_at' => now()->addDays(1), + 'max_claimants' => 10, + 'claimed_amount' => 0, + 'total_amount' => 5000, + 'distribute_type' => 'fixed', + 'send_at' => now(), + 'repeat_type' => 'once', + 'target_type' => 'all', + ]); + + HolidayClaim::create([ + 'event_id' => $event->id, + 'user_id' => $user->id, + 'amount' => 500, + 'claimed_at' => now(), + ]); + + $response = $this->actingAs($user)->getJson(route('holiday.status', ['event' => $event->id])); + + $response->assertStatus(200); + $response->assertJson([ + 'claimable' => true, + 'amount' => 500, + ]); + } + + public function test_can_claim_holiday_bonus() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 100]); + $event = HolidayEvent::create([ + 'name' => 'Test Holiday', + 'status' => 'active', + 'expires_at' => now()->addDays(1), + 'max_claimants' => 10, + 'claimed_amount' => 0, + 'total_amount' => 5000, + 'distribute_type' => 'fixed', + 'send_at' => now(), + 'repeat_type' => 'once', + 'target_type' => 'all', + ]); + + HolidayClaim::create([ + 'event_id' => $event->id, + 'user_id' => $user->id, + 'amount' => 500, + 'claimed_at' => now(), + ]); + + $response = $this->actingAs($user)->postJson(route('holiday.claim', ['event' => $event->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + // Verify currency incremented + $this->assertEquals(600, $user->fresh()->jjb); + + // Verify claim is deleted + $this->assertDatabaseMissing('holiday_claims', [ + 'event_id' => $event->id, + 'user_id' => $user->id, + ]); + } + + public function test_cannot_claim_if_not_in_list() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + $event = HolidayEvent::create([ + 'name' => 'Test Holiday', + 'status' => 'active', + 'expires_at' => now()->addDays(1), + 'max_claimants' => 10, + 'claimed_amount' => 0, + 'total_amount' => 5000, + 'distribute_type' => 'fixed', + 'send_at' => now(), + 'repeat_type' => 'once', + 'target_type' => 'all', + ]); + + $response = $this->actingAs($user)->postJson(route('holiday.claim', ['event' => $event->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => false, 'message' => '您不在本次福利名单内,或活动已结束。']); + } + + public function test_cannot_claim_if_expired() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + $event = HolidayEvent::create([ + 'name' => 'Test Holiday', + 'status' => 'completed', + 'expires_at' => now()->subDays(1), + 'max_claimants' => 10, + 'claimed_amount' => 0, + 'total_amount' => 5000, + 'distribute_type' => 'fixed', + 'send_at' => now(), + 'repeat_type' => 'once', + 'target_type' => 'all', + ]); + + HolidayClaim::create([ + 'event_id' => $event->id, + 'user_id' => $user->id, + 'amount' => 500, + 'claimed_at' => now(), + ]); + + $response = $this->actingAs($user)->postJson(route('holiday.claim', ['event' => $event->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => false, 'message' => '活动已结束或已过期。']); + } +} diff --git a/tests/Feature/HorseRaceControllerTest.php b/tests/Feature/HorseRaceControllerTest.php new file mode 100644 index 0000000..517715b --- /dev/null +++ b/tests/Feature/HorseRaceControllerTest.php @@ -0,0 +1,185 @@ + 'horse_racing'], + [ + 'name' => 'Horse Racing', + 'icon' => 'horse', + 'description' => 'Horse Racing Game', + 'enabled' => true, + 'params' => [ + 'min_bet' => 100, + 'max_bet' => 100000, + 'house_take_percent' => 5, + ], + ] + ); + } + + public function test_can_get_current_race() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $race = HorseRace::create([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'horses' => [ + ['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'], + ['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'], + ], + 'total_bets' => 0, + 'total_pool' => 0, + ]); + + $response = $this->actingAs($user)->getJson(route('horse-race.current')); + + $response->assertStatus(200); + $response->assertJsonStructure(['race' => ['id', 'status', 'bet_closes_at', 'horses']]); + $this->assertEquals($race->id, $response->json('race.id')); + } + + public function test_can_bet() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 500]); + + $race = HorseRace::create([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'horses' => [ + ['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'], + ['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'], + ], + 'total_bets' => 0, + 'total_pool' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('horse-race.bet'), [ + 'race_id' => $race->id, + 'horse_id' => 1, + 'amount' => 100, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + $this->assertEquals(400, $user->fresh()->jjb); + $this->assertDatabaseHas('horse_bets', [ + 'race_id' => $race->id, + 'user_id' => $user->id, + 'horse_id' => 1, + 'amount' => 100, + ]); + } + + public function test_cannot_bet_out_of_range() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 500]); + + $race = HorseRace::create([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'horses' => [ + ['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'], + ['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'], + ], + 'total_bets' => 0, + 'total_pool' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('horse-race.bet'), [ + 'race_id' => $race->id, + 'horse_id' => 1, + 'amount' => 50, // Less than min_bet + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_cannot_bet_twice_in_same_race() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 500]); + + $race = HorseRace::create([ + 'status' => 'betting', + 'bet_opens_at' => now(), + 'bet_closes_at' => now()->addMinutes(1), + 'horses' => [ + ['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'], + ['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'], + ], + 'total_bets' => 0, + 'total_pool' => 0, + ]); + + HorseBet::forceCreate([ + 'race_id' => $race->id, + 'user_id' => $user->id, + 'horse_id' => 1, + 'amount' => 100, + 'status' => 'pending', + ]); + + $response = $this->actingAs($user)->postJson(route('horse-race.bet'), [ + 'race_id' => $race->id, + 'horse_id' => 2, + 'amount' => 100, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_can_get_history() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + HorseRace::create([ + 'status' => 'settled', + 'bet_opens_at' => now()->subMinutes(2), + 'bet_closes_at' => now()->subMinutes(1), + 'settled_at' => now(), + 'winner_horse_id' => 1, + 'horses' => [ + ['id' => 1, 'name' => 'Horse A', 'emoji' => '🐎'], + ['id' => 2, 'name' => 'Horse B', 'emoji' => '🏇'], + ], + 'total_bets' => 1, + 'total_pool' => 100, + ]); + + $response = $this->actingAs($user)->getJson(route('horse-race.history')); + + $response->assertStatus(200); + $response->assertJsonStructure(['history']); + $this->assertCount(1, $response->json('history')); + } +} diff --git a/tests/Feature/InviteControllerTest.php b/tests/Feature/InviteControllerTest.php new file mode 100644 index 0000000..43d004e --- /dev/null +++ b/tests/Feature/InviteControllerTest.php @@ -0,0 +1,43 @@ +create(); + + $response = $this->get(route('invite.link', $inviter->id)); + + $response->assertRedirect(route('home')); + $response->assertCookie('inviter_id', $inviter->id); + } + + public function test_handle_ignores_invalid_inviter() + { + $response = $this->get(route('invite.link', 99999)); + + $response->assertRedirect(route('home')); + $response->assertCookieMissing('inviter_id'); + } + + public function test_leaderboard_displays_inviters() + { + $inviter = User::factory()->create(); + $user = User::factory()->create(); + + User::factory()->count(2)->create(['inviter_id' => $inviter->id]); + + $response = $this->actingAs($user)->get(route('invite.leaderboard')); + + $response->assertStatus(200); + $response->assertViewIs('invite.leaderboard'); + } +} diff --git a/tests/Feature/LeaderboardControllerTest.php b/tests/Feature/LeaderboardControllerTest.php new file mode 100644 index 0000000..69eb075 --- /dev/null +++ b/tests/Feature/LeaderboardControllerTest.php @@ -0,0 +1,73 @@ +create(['exp_num' => 10, 'jjb' => 100, 'meili' => 5]); + + Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']); + Sysparam::updateOrCreate(['alias' => 'leaderboard_limit'], ['body' => '20']); + + // Create users for leaderboard + User::factory()->create(['user_level' => 10, 'exp_num' => 100, 'jjb' => 1000, 'meili' => 50]); + User::factory()->create(['user_level' => 5, 'exp_num' => 50, 'jjb' => 500, 'meili' => 20]); + + // Super admin should be hidden + User::factory()->create(['user_level' => 100, 'exp_num' => 10000, 'jjb' => 100000, 'meili' => 5000]); + + $response = $this->actingAs($user)->get(route('leaderboard.index')); + + $response->assertStatus(200); + $response->assertViewIs('leaderboard.index'); + + $topLevels = $response->viewData('topLevels'); + $this->assertNotNull($topLevels); + $this->assertEquals(3, $topLevels->count()); // Excludes super admin + $this->assertEquals(10, $topLevels->first()->user_level); + + $topExp = $response->viewData('topExp'); + $this->assertNotNull($topExp); + $this->assertEquals(3, $topExp->count()); + $this->assertEquals(100, $topExp->first()->exp_num); + + $topWealth = $response->viewData('topWealth'); + $this->assertNotNull($topWealth); + $this->assertEquals(3, $topWealth->count()); + $this->assertEquals(1000, $topWealth->first()->jjb); + + $topCharm = $response->viewData('topCharm'); + $this->assertNotNull($topCharm); + $this->assertEquals(3, $topCharm->count()); + $this->assertEquals(50, $topCharm->first()->meili); + } + + public function test_can_view_today_leaderboard() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('leaderboard.today')); + + $response->assertStatus(200); + $response->assertViewIs('leaderboard.today'); + } + + public function test_can_view_my_currency_logs() + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('currency.my-logs')); + + $response->assertStatus(200); + $response->assertViewIs('leaderboard.my-logs'); + } +} diff --git a/tests/Feature/LotteryControllerTest.php b/tests/Feature/LotteryControllerTest.php new file mode 100644 index 0000000..3abd09f --- /dev/null +++ b/tests/Feature/LotteryControllerTest.php @@ -0,0 +1,214 @@ + 'lottery'], + [ + 'name' => 'Lottery', + 'icon' => 'lottery', + 'description' => 'Lottery Game', + 'enabled' => true, + 'params' => [ + 'ticket_price' => 100, + ], + ] + ); + } + + public function test_can_get_current() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $issue = LotteryIssue::create([ + 'issue_no' => '2026101', + 'status' => 'open', + 'red1' => 0, + 'red2' => 0, + 'red3' => 0, + 'blue' => 0, + 'pool_amount' => 5000, + 'carry_amount' => 0, + 'is_super_issue' => false, + 'no_winner_streak' => 0, + 'total_tickets' => 0, + 'payout_amount' => 0, + 'sell_closes_at' => now()->addMinutes(1), + 'draw_at' => now()->addMinutes(2), + ]); + + $response = $this->actingAs($user)->getJson(route('lottery.current')); + + $response->assertStatus(200); + $response->assertJsonStructure(['issue' => ['id', 'issue_no', 'status', 'pool_amount']]); + $this->assertEquals($issue->id, $response->json('issue.id')); + } + + public function test_can_quick_pick() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->getJson(route('lottery.quick-pick', ['count' => 2])); + + $response->assertStatus(200); + $response->assertJsonStructure(['numbers']); + $this->assertCount(2, $response->json('numbers')); + } + + public function test_cannot_buy_without_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 50]); + + LotteryIssue::create([ + 'issue_no' => '2026101', + 'status' => 'open', + 'red1' => 0, + 'red2' => 0, + 'red3' => 0, + 'blue' => 0, + 'pool_amount' => 5000, + 'carry_amount' => 0, + 'is_super_issue' => false, + 'no_winner_streak' => 0, + 'total_tickets' => 0, + 'payout_amount' => 0, + 'sell_closes_at' => now()->addMinutes(1), + 'draw_at' => now()->addMinutes(2), + ]); + + $response = $this->actingAs($user)->postJson(route('lottery.buy'), [ + 'numbers' => [ + [ + 'reds' => [1, 2, 3], + 'blue' => 4, + ], + ], + 'quick_pick' => false, + ]); + + $response->assertStatus(422); // Exception thrown with 422 + } + + public function test_can_buy_with_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 200]); + + LotteryIssue::create([ + 'issue_no' => '2026101', + 'status' => 'open', + 'red1' => 0, + 'red2' => 0, + 'red3' => 0, + 'blue' => 0, + 'pool_amount' => 5000, + 'carry_amount' => 0, + 'is_super_issue' => false, + 'no_winner_streak' => 0, + 'total_tickets' => 0, + 'payout_amount' => 0, + 'sell_closes_at' => now()->addMinutes(1), + 'draw_at' => now()->addMinutes(2), + ]); + + $response = $this->actingAs($user)->postJson(route('lottery.buy'), [ + 'numbers' => [ + [ + 'reds' => [1, 2, 3], + 'blue' => 4, + ], + ], + 'quick_pick' => false, + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + $this->assertEquals(100, $user->fresh()->jjb); // cost is 100 per ticket + } + + public function test_can_get_history() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + LotteryIssue::create([ + 'issue_no' => '2026100', + 'status' => 'settled', + 'red1' => 1, + 'red2' => 2, + 'red3' => 3, + 'blue' => 4, + 'pool_amount' => 5000, + 'carry_amount' => 0, + 'is_super_issue' => false, + 'no_winner_streak' => 0, + 'total_tickets' => 10, + 'payout_amount' => 1000, + 'draw_at' => now(), + ]); + + $response = $this->actingAs($user)->getJson(route('lottery.history')); + + $response->assertStatus(200); + $response->assertJsonStructure(['issues']); + $this->assertCount(1, $response->json('issues')); + } + + public function test_can_get_my_tickets() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $issue = LotteryIssue::create([ + 'issue_no' => '2026100', + 'status' => 'settled', + 'red1' => 1, + 'red2' => 2, + 'red3' => 3, + 'blue' => 4, + 'pool_amount' => 5000, + 'carry_amount' => 0, + 'is_super_issue' => false, + 'no_winner_streak' => 0, + 'total_tickets' => 10, + 'payout_amount' => 1000, + 'draw_at' => now(), + ]); + + LotteryTicket::create([ + 'issue_id' => $issue->id, + 'user_id' => $user->id, + 'red1' => 1, + 'red2' => 2, + 'red3' => 3, + 'blue' => 4, + 'amount' => 100, + 'is_quick_pick' => 0, + ]); + + $response = $this->actingAs($user)->getJson(route('lottery.my')); + + $response->assertStatus(200); + $response->assertJsonStructure(['tickets']); + $this->assertCount(1, $response->json('tickets')); + } +} diff --git a/tests/Feature/MarriageControllerTest.php b/tests/Feature/MarriageControllerTest.php new file mode 100644 index 0000000..110b871 --- /dev/null +++ b/tests/Feature/MarriageControllerTest.php @@ -0,0 +1,269 @@ + 'test', + 'hyname1' => 'test1', + 'hytime' => now(), + 'hygb' => 'test', + 'hyjb' => 'test', + 'i' => 0, + ]; + } + + public function test_can_get_divorce_config() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->getJson(route('marriage.divorce-config')); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'mutual_charm_penalty', + 'forced_charm_penalty', + 'mutual_cooldown_days', + 'forced_cooldown_days', + ]); + } + + public function test_can_get_status_unmarried() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->getJson(route('marriage.status')); + + $response->assertStatus(200); + $response->assertJson(['married' => false]); + } + + public function test_can_get_status_married() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $user->id, + 'partner_id' => $partner->id, + 'status' => 'married', + 'married_at' => now(), + 'intimacy' => 100, + ], $this->createLegacyMarriageData())); + + $response = $this->actingAs($user)->getJson(route('marriage.status')); + + $response->assertStatus(200); + $response->assertJson([ + 'married' => true, + 'status' => 'married', + ]); + $response->assertJsonPath('marriage.partner.id', $partner->id); + } + + public function test_can_get_target_status() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $target */ + $target = User::factory()->create(); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + Marriage::create(array_merge([ + 'user_id' => $target->id, + 'partner_id' => $partner->id, + 'status' => 'married', + 'married_at' => now(), + ], $this->createLegacyMarriageData())); + + $response = $this->actingAs($user)->getJson(route('marriage.target-status', [ + 'username' => $target->username, + ])); + + $response->assertStatus(200); + $response->assertJson([ + 'married' => true, + ]); + $response->assertJsonPath('marriage.partner_name', $partner->username); + } + + public function test_can_get_my_rings() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $ring = ShopItem::create([ + 'name' => 'Diamond Ring', + 'slug' => 'diamond_ring', + 'type' => 'ring', + 'price' => 1000, + 'currency' => 'gold', + 'use_type' => 'permanent', + ]); + + $purchase = UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $ring->id, + 'status' => 'active', + 'number' => 1, + 'amount' => 1000, + 'currency' => 'gold', + ]); + + $response = $this->actingAs($user)->getJson(route('marriage.rings')); + + $response->assertStatus(200); + $response->assertJsonStructure(['status', 'rings']); + $this->assertCount(1, $response->json('rings')); + $this->assertEquals($purchase->id, $response->json('rings.0.purchase_id')); + } + + public function test_can_propose() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10000, 'sex' => 1]); + /** @var \App\Models\User $target */ + $target = User::factory()->create(['sex' => 2]); + + $ring = ShopItem::create([ + 'name' => 'Diamond Ring', + 'slug' => 'diamond_ring', + 'type' => 'ring', + 'price' => 1000, + 'currency' => 'gold', + 'use_type' => 'permanent', + ]); + + $purchase = UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $ring->id, + 'status' => 'active', + 'number' => 1, + 'amount' => 1000, + 'currency' => 'gold', + ]); + + $response = $this->actingAs($user)->postJson(route('marriage.propose'), [ + 'target_username' => $target->username, + 'ring_purchase_id' => $purchase->id, + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + Event::assertDispatched(\App\Events\MarriageProposed::class); + } + + public function test_can_accept_proposal() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + $ring = ShopItem::create([ + 'name' => 'Diamond', + 'slug' => 'diamond', + 'type' => 'ring', + 'price' => 1000, + 'currency' => 'gold', + 'use_type' => 'permanent', + ]); + + $purchase = UserPurchase::create([ + 'user_id' => $partner->id, + 'shop_item_id' => $ring->id, + 'status' => 'active', + 'number' => 1, + 'amount' => 1000, + 'currency' => 'gold', + ]); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $partner->id, + 'partner_id' => $user->id, + 'status' => 'pending', + 'ring_purchase_id' => $purchase->id, + 'proposed_at' => now(), + ], $this->createLegacyMarriageData())); + + $response = $this->actingAs($user)->postJson(route('marriage.accept', ['marriage' => $marriage->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + Event::assertDispatched(\App\Events\MarriageAccepted::class); + } + + public function test_can_reject_proposal() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $partner->id, + 'partner_id' => $user->id, + 'status' => 'pending', + 'proposed_at' => now(), + ], $this->createLegacyMarriageData())); + + $response = $this->actingAs($user)->postJson(route('marriage.reject', ['marriage' => $marriage->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + Event::assertDispatched(\App\Events\MarriageRejected::class); + } + + public function test_can_divorce_mutual() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $user->id, + 'partner_id' => $partner->id, + 'status' => 'married', + 'married_at' => now()->subDays(10), // Needs to have marriage age + ], $this->createLegacyMarriageData())); + + $response = $this->actingAs($user)->postJson(route('marriage.divorce', ['marriage' => $marriage->id]), [ + 'type' => 'mutual', + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + Event::assertDispatched(\App\Events\MarriageDivorceRequested::class); + } +} diff --git a/tests/Feature/RedPacketControllerTest.php b/tests/Feature/RedPacketControllerTest.php new file mode 100644 index 0000000..d7ee26e --- /dev/null +++ b/tests/Feature/RedPacketControllerTest.php @@ -0,0 +1,165 @@ + 'superlevel'], ['body' => '100']); + } + + public function test_normal_user_cannot_send_red_packet() + { + $user = User::factory()->create(['user_level' => 10]); + + $response = $this->actingAs($user)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $response->assertStatus(403); + $response->assertJson(['status' => 'error']); + } + + public function test_superadmin_can_send_red_packet() + { + $admin = User::factory()->create(['user_level' => 100]); + + $response = $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + $this->assertDatabaseHas('red_packet_envelopes', [ + 'sender_id' => $admin->id, + 'room_id' => 1, + 'type' => 'gold', + 'status' => 'active', + ]); + + $envelope = RedPacketEnvelope::first(); + // Check Redis for parts + $this->assertEquals(10, Redis::llen("red_packet:{$envelope->id}:amounts")); + } + + public function test_cannot_send_multiple_active_packets_in_same_room() + { + $admin = User::factory()->create(['user_level' => 100]); + + $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $response = $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $response->assertStatus(422); + } + + public function test_user_can_claim_red_packet() + { + $admin = User::factory()->create(['user_level' => 100]); + $user = User::factory()->create(['jjb' => 100]); + + // Send packet + $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $envelope = RedPacketEnvelope::first(); + + // Claim packet + $response = $this->actingAs($user)->postJson(route('red_packet.claim', ['envelopeId' => $envelope->id]), [ + 'room_id' => 1, + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + $this->assertDatabaseHas('red_packet_claims', [ + 'envelope_id' => $envelope->id, + 'user_id' => $user->id, + ]); + + // Verify currency incremented + $this->assertGreaterThan(100, $user->fresh()->jjb); + } + + public function test_user_cannot_claim_same_packet_twice() + { + $admin = User::factory()->create(['user_level' => 100]); + $user = User::factory()->create(); + + $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $envelope = RedPacketEnvelope::first(); + + // First claim + $this->actingAs($user)->postJson(route('red_packet.claim', ['envelopeId' => $envelope->id]), [ + 'room_id' => 1, + ]); + + // Second claim + $response = $this->actingAs($user)->postJson(route('red_packet.claim', ['envelopeId' => $envelope->id]), [ + 'room_id' => 1, + ]); + + $response->assertStatus(422); + $response->assertJson(['message' => '您已经领过这个礼包了']); + } + + public function test_can_check_packet_status() + { + $admin = User::factory()->create(['user_level' => 100]); + $user = User::factory()->create(); + + $this->actingAs($admin)->postJson(route('command.red_packet.send'), [ + 'room_id' => 1, + 'type' => 'gold', + ]); + + $envelope = RedPacketEnvelope::first(); + + $response = $this->actingAs($user)->getJson(route('red_packet.status', ['envelopeId' => $envelope->id])); + + $response->assertStatus(200); + $response->assertJson([ + 'status' => 'success', + 'has_claimed' => false, + 'is_expired' => false, + ]); + + // Claim it + $this->actingAs($user)->postJson(route('red_packet.claim', ['envelopeId' => $envelope->id]), [ + 'room_id' => 1, + ]); + + $response2 = $this->actingAs($user)->getJson(route('red_packet.status', ['envelopeId' => $envelope->id])); + $response2->assertJson([ + 'status' => 'success', + 'has_claimed' => true, + ]); + } +} diff --git a/tests/Feature/RoomControllerTest.php b/tests/Feature/RoomControllerTest.php new file mode 100644 index 0000000..01bc038 --- /dev/null +++ b/tests/Feature/RoomControllerTest.php @@ -0,0 +1,201 @@ +create(); + $room = Room::create([ + 'room_name' => 'TestRoom', + 'room_owner' => $user->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($user)->get(route('rooms.index')); + + $response->assertStatus(200); + $response->assertSee('TestRoom'); + } + + public function test_can_create_room_if_level_is_high_enough() + { + // Require level 10 + $user = User::factory()->create(['user_level' => 10]); + + $response = $this->actingAs($user)->post(route('rooms.store'), [ + 'name' => 'NewRoom', + 'description' => 'Test Description', + ]); + + $response->assertRedirect(route('rooms.index')); + $this->assertDatabaseHas('rooms', [ + 'room_name' => 'NewRoom', + 'room_owner' => $user->username, + ]); + } + + public function test_cannot_create_room_if_level_too_low() + { + $user = User::factory()->create(['user_level' => 9]); + + $response = $this->actingAs($user)->post(route('rooms.store'), [ + 'name' => 'NewRoom', + ]); + + $response->assertStatus(403); + $this->assertDatabaseMissing('rooms', [ + 'room_name' => 'NewRoom', + ]); + } + + public function test_room_owner_can_update_room() + { + $user = User::factory()->create(); + $room = Room::create([ + 'room_name' => 'OldName', + 'room_owner' => $user->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($user)->put(route('rooms.update', $room->id), [ + 'name' => 'NewName', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + 'room_name' => 'NewName', + ]); + } + + public function test_non_owner_cannot_update_room() + { + $owner = User::factory()->create(['user_level' => 1]); + $attacker = User::factory()->create(['user_level' => 1]); + $room = Room::create([ + 'room_name' => 'OldName', + 'room_owner' => $owner->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($attacker)->put(route('rooms.update', $room->id), [ + 'name' => 'HackName', + ]); + + $response->assertStatus(403); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + 'room_name' => 'OldName', + ]); + } + + public function test_admin_can_update_any_room() + { + $owner = User::factory()->create(['user_level' => 1]); + $admin = User::factory()->create(['user_level' => 15]); + $room = Room::create([ + 'room_name' => 'OldName', + 'room_owner' => $owner->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($admin)->put(route('rooms.update', $room->id), [ + 'name' => 'AdminRoom', + ]); + + $response->assertRedirect(); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + 'room_name' => 'AdminRoom', + ]); + } + + public function test_room_owner_can_destroy_non_system_room() + { + $user = User::factory()->create(); + $room = Room::create([ + 'room_name' => 'ToDelete', + 'room_owner' => $user->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($user)->delete(route('rooms.destroy', $room->id)); + + $response->assertRedirect(route('rooms.index')); + $this->assertDatabaseMissing('rooms', [ + 'id' => $room->id, + ]); + } + + public function test_cannot_destroy_system_room() + { + $user = User::factory()->create(['user_level' => 20]); + $room = Room::create([ + 'room_name' => 'SysRoom', + 'room_owner' => $user->username, + 'room_keep' => true, + ]); + + $response = $this->actingAs($user)->delete(route('rooms.destroy', $room->id)); + + $response->assertStatus(403); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + ]); + } + + public function test_room_owner_can_transfer_room() + { + $owner = User::factory()->create(); + $target = User::factory()->create(); + + $room = Room::create([ + 'room_name' => 'TransferMe', + 'room_owner' => $owner->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($owner)->post(route('rooms.transfer', $room->id), [ + 'target_username' => $target->username, + ]); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + 'room_owner' => $target->username, + ]); + } + + public function test_cannot_transfer_to_invalid_user() + { + $owner = User::factory()->create(); + + $room = Room::create([ + 'room_name' => 'TransferMe', + 'room_owner' => $owner->username, + 'room_keep' => false, + ]); + + $response = $this->actingAs($owner)->post(route('rooms.transfer', $room->id), [ + 'target_username' => 'ghost_user_999', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('error'); + $this->assertDatabaseHas('rooms', [ + 'id' => $room->id, + 'room_owner' => $owner->username, + ]); + } +} diff --git a/tests/Feature/ShopControllerTest.php b/tests/Feature/ShopControllerTest.php new file mode 100644 index 0000000..679d490 --- /dev/null +++ b/tests/Feature/ShopControllerTest.php @@ -0,0 +1,169 @@ +create(); + + $activeItem = ShopItem::create([ + 'name' => 'Active', + 'slug' => 'active_item', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => true, + ]); + + $inactiveItem = ShopItem::create([ + 'name' => 'Inactive', + 'slug' => 'inactive_item', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => false, + ]); + + $response = $this->actingAs($user)->getJson(route('shop.items')); + + $response->assertStatus(200); + + $responseItems = collect($response->json('items')); + $this->assertTrue($responseItems->contains('id', $activeItem->id)); + $this->assertFalse($responseItems->contains('id', $inactiveItem->id)); + } + + public function test_can_buy_one_time_item() + { + $user = User::factory()->create(['jjb' => 500]); + + $item = ShopItem::firstOrCreate( + ['slug' => 'rename_card_test'], + [ + 'name' => 'Rename Card Test', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => true, + ]); + + $response = $this->actingAs($user)->postJson(route('shop.buy'), [ + 'item_id' => $item->id, + 'room_id' => 1, + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + $this->assertDatabaseHas('user_purchases', [ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + ]); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'jjb' => 400, + ]); + } + + public function test_cannot_buy_if_insufficient_funds() + { + $user = User::factory()->create(['jjb' => 50]); + + $item = ShopItem::firstOrCreate( + ['slug' => 'rename_card_test'], + [ + 'name' => 'Rename Card Test', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => true, + ]); + + $response = $this->actingAs($user)->postJson(route('shop.buy'), [ + 'item_id' => $item->id, + ]); + + $response->assertStatus(400); + $response->assertJson(['status' => 'error']); + + $this->assertDatabaseMissing('user_purchases', [ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + ]); + } + + public function test_cannot_buy_inactive_item() + { + $user = User::factory()->create(['jjb' => 500]); + + $item = ShopItem::create([ + 'name' => 'Old Card', + 'slug' => 'old_card', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => false, + ]); + + $response = $this->actingAs($user)->postJson(route('shop.buy'), [ + 'item_id' => $item->id, + ]); + + $response->assertStatus(400); + + $this->assertDatabaseMissing('user_purchases', [ + 'user_id' => $user->id, + ]); + } + + public function test_can_use_rename_card() + { + $user = User::factory()->create(['username' => 'OldName']); + + // Actually the service hardcodes 'rename_card' slug check: $item->slug === 'rename_card' + // So we MUST use 'rename_card' + $item = ShopItem::firstOrCreate( + ['slug' => 'rename_card'], + [ + 'name' => 'Rename Card', + 'type' => 'one_time', + 'price' => 100, + 'is_active' => true, + ]); + + UserPurchase::create([ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'active', + 'used_at' => null, + 'cost_amount' => 100, + 'currency_type' => 'gold', + ]); + + $response = $this->actingAs($user)->postJson(route('shop.rename'), [ + 'new_name' => 'NewName', + ]); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + // Assert user's name is updated + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'username' => 'NewName', + ]); + + // Assert card is used + $this->assertDatabaseHas('user_purchases', [ + 'user_id' => $user->id, + 'shop_item_id' => $item->id, + 'status' => 'used', + ]); + } +} diff --git a/tests/Feature/SlotMachineControllerTest.php b/tests/Feature/SlotMachineControllerTest.php new file mode 100644 index 0000000..6b13393 --- /dev/null +++ b/tests/Feature/SlotMachineControllerTest.php @@ -0,0 +1,143 @@ + 'slot_machine'], + [ + 'name' => 'Slot Machine', + 'icon' => 'slot', + 'description' => 'Slot Machine Game', + 'enabled' => true, + 'params' => [ + 'cost_per_spin' => 100, + 'daily_limit' => 100, + 'jackpot_payout' => 100, + 'triple_payout' => 50, + 'same_payout' => 10, + 'pair_payout' => 2, + 'curse_enabled' => true, + ], + ] + ); + } + + public function test_can_get_info() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + $response = $this->actingAs($user)->getJson(route('slot.info')); + + $response->assertStatus(200); + $response->assertJson([ + 'enabled' => true, + 'cost_per_spin' => 100, + 'daily_limit' => 100, + 'used_today' => 0, + ]); + $response->assertJsonStructure(['symbols']); + } + + public function test_can_spin_enough_gold() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 200]); + + $response = $this->actingAs($user)->postJson(route('slot.spin')); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + $this->assertDatabaseHas('slot_machine_logs', [ + 'user_id' => $user->id, + 'cost' => 100, + ]); + } + + public function test_cannot_spin_without_enough_gold() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 50]); // Need 100 + + $response = $this->actingAs($user)->postJson(route('slot.spin')); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_cannot_spin_exceed_daily_limit() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 2000]); + + // Mock daily limit 1 + GameConfig::updateOrCreate( + ['game_key' => 'slot_machine'], + [ + 'name' => 'Slot Machine', + 'icon' => 'slot', + 'description' => 'Slot Mac', + 'enabled' => true, + 'params' => [ + 'cost_per_spin' => 100, + 'daily_limit' => 1, + ], + ] + ); + + SlotMachineLog::create([ + 'user_id' => $user->id, + 'reel1' => 'cherry', + 'reel2' => 'lemon', + 'reel3' => 'orange', + 'result_type' => 'miss', + 'cost' => 100, + 'payout' => 0, + ]); + + $response = $this->actingAs($user)->postJson(route('slot.spin')); + + $response->assertStatus(200); + $response->assertJson(['ok' => false]); + } + + public function test_can_get_history() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + SlotMachineLog::create([ + 'user_id' => $user->id, + 'reel1' => 'cherry', + 'reel2' => 'cherry', + 'reel3' => 'cherry', + 'result_type' => 'triple', + 'cost' => 100, + 'payout' => 1000, + ]); + + $response = $this->actingAs($user)->getJson(route('slot.history')); + + $response->assertStatus(200); + $response->assertJsonStructure(['history']); + $this->assertCount(1, $response->json('history')); + } +} diff --git a/tests/Feature/UserControllerTest.php b/tests/Feature/UserControllerTest.php new file mode 100644 index 0000000..5dc0145 --- /dev/null +++ b/tests/Feature/UserControllerTest.php @@ -0,0 +1,255 @@ + 'superlevel'], ['body' => '100']); + Sysparam::updateOrCreate(['alias' => 'level_kick'], ['body' => '15']); + Sysparam::updateOrCreate(['alias' => 'level_mute'], ['body' => '15']); + Sysparam::updateOrCreate(['alias' => 'level_ban'], ['body' => '15']); + Sysparam::updateOrCreate(['alias' => 'level_banip'], ['body' => '15']); + Sysparam::updateOrCreate(['alias' => 'smtp_enabled'], ['body' => '1']); // Allow email changing in tests + } + + public function test_can_view_user_profile() + { + $user = User::factory()->create([ + 'username' => 'testuser', + 'user_level' => 10, + ]); + + $this->actingAs($user); + + $response = $this->getJson("/user/{$user->username}"); + + $response->assertStatus(200) + ->assertJsonPath('data.username', 'testuser') + ->assertJsonPath('data.user_level', 10); + } + + public function test_can_update_profile_without_email_change() + { + $user = User::factory()->create([ + 'username' => 'testuser', + 'email' => 'old@example.com', + 'sign' => 'old sign', + ]); + + $this->actingAs($user); + + $response = $this->putJson('/user/profile', [ + 'email' => 'old@example.com', + 'sign' => 'new sign', + 'sex' => 1, + 'headface' => 'avatar1.png', + ]); + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + + $user->refresh(); + $this->assertEquals('new sign', $user->sign); + } + + public function test_cannot_update_email_without_verification_code() + { + $user = User::factory()->create([ + 'username' => 'testuser', + 'email' => 'old@example.com', + ]); + + $this->actingAs($user); + + $response = $this->putJson('/user/profile', [ + 'email' => 'new@example.com', + 'sex' => 1, + 'headface' => 'avatar1.png', + ]); + + $response->assertStatus(422) + ->assertJsonPath('status', 'error') + ->assertJsonPath('message', '新邮箱需要验证码,请先获取并填写验证码。'); + } + + public function test_can_update_email_with_valid_code() + { + $user = User::factory()->create([ + 'username' => 'testuser', + 'email' => 'old@example.com', + ]); + + Cache::put("email_verify_code_{$user->id}_new@example.com", '123456', 5); + + $this->actingAs($user); + + $response = $this->putJson('/user/profile', [ + 'email' => 'new@example.com', + 'email_code' => '123456', + 'sex' => 1, + 'headface' => 'avatar1.png', + ]); + + $response->assertStatus(200); + + $user->refresh(); + $this->assertEquals('new@example.com', $user->email); + } + + public function test_can_change_password() + { + $user = User::factory()->create([ + 'username' => 'testuser', + 'password' => Hash::make('oldpassword'), + ]); + + $this->actingAs($user); + + $response = $this->putJson('/user/password', [ + 'old_password' => 'oldpassword', + 'new_password' => 'newpassword123', + 'new_password_confirmation' => 'newpassword123', + ]); + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + + $user->refresh(); + $this->assertTrue(Hash::check('newpassword123', $user->password)); + } + + public function test_admin_can_kick_user() + { + $admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + $this->actingAs($admin); + + $response = $this->postJson("/user/{$target->username}/kick", [ + 'room_id' => $room->id, + ]); + + $response->assertStatus(200) + ->assertJsonPath('status', 'success'); + } + + public function test_low_level_user_cannot_kick() + { + $user = User::factory()->create(['username' => 'user', 'user_level' => 1]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + $this->actingAs($user); + + $response = $this->postJson("/user/{$target->username}/kick", [ + 'room_id' => $room->id, + ]); + + $response->assertStatus(403); + } + + public function test_room_master_can_kick() + { + $user = User::factory()->create(['username' => 'user', 'user_level' => 2]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'user']); // Master is 'user' + + $this->actingAs($user); + + $response = $this->postJson("/user/{$target->username}/kick", [ + 'room_id' => $room->id, + ]); + + if ($response->status() !== 200) { + dump($response->json()); + } + $response->assertStatus(200); + } + + public function test_cannot_kick_higher_level() + { + $admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]); + $superadmin = User::factory()->create(['username' => 'superadmin', 'user_level' => 100]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + $this->actingAs($admin); + + $response = $this->postJson("/user/{$superadmin->username}/kick", [ + 'room_id' => $room->id, + ]); + + $response->assertStatus(403); + } + + public function test_admin_can_mute_user() + { + $admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + Redis::shouldReceive('setex')->once(); + + $this->actingAs($admin); + + $response = $this->postJson("/user/{$target->username}/mute", [ + 'room_id' => $room->id, + 'duration' => 10, + ]); + + $response->assertStatus(200); + } + + public function test_admin_can_ban_user() + { + $admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1]); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + $this->actingAs($admin); + + $response = $this->postJson("/user/{$target->username}/ban", [ + 'room_id' => $room->id, + ]); + + $response->assertStatus(200); + + $target->refresh(); + $this->assertEquals(-1, $target->user_level); + } + + public function test_admin_can_ban_ip() + { + $admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]); + $target = User::factory()->create(['username' => 'target', 'user_level' => 1, 'last_ip' => '192.168.1.100']); + $room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']); + + Redis::shouldReceive('sadd')->with('banned_ips', '192.168.1.100')->once(); + + $this->actingAs($admin); + + $response = $this->postJson("/user/{$target->username}/banip", [ + 'room_id' => $room->id, + ]); + + $response->assertStatus(200); + + $target->refresh(); + $this->assertEquals(-1, $target->user_level); + } +} diff --git a/tests/Feature/WeddingControllerTest.php b/tests/Feature/WeddingControllerTest.php new file mode 100644 index 0000000..229ff07 --- /dev/null +++ b/tests/Feature/WeddingControllerTest.php @@ -0,0 +1,244 @@ + 'test', + 'hyname1' => 'test1', + 'hytime' => now(), + 'hygb' => 'test', + 'hyjb' => 'test', + 'i' => 0, + ]; + } + + public function test_can_get_tiers() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + WeddingTier::forceCreate([ + 'tier' => 1, + 'name' => 'Basic Wedding', + 'icon' => 'basic', + 'amount' => 1000, + 'description' => 'A basic wedding', + 'is_active' => true, + ]); + + $response = $this->actingAs($user)->getJson(route('wedding.tiers')); + + $response->assertStatus(200); + $response->assertJsonStructure(['tiers']); + $this->assertCount(1, $response->json('tiers')); + } + + public function test_can_setup_wedding() + { + Event::fake(); + + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 10000]); + /** @var \App\Models\User $partner */ + $partner = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $user->id, + 'partner_id' => $partner->id, + 'status' => 'married', + 'married_at' => now(), + ], $this->createLegacyMarriageData())); + + $tier = WeddingTier::forceCreate([ + 'tier' => 1, + 'name' => 'Basic Wedding', + 'icon' => 'basic', + 'amount' => 1000, + 'description' => 'A basic wedding', + 'is_active' => true, + ]); + + $response = $this->actingAs($user)->postJson(route('wedding.setup', ['marriage' => $marriage->id]), [ + 'tier_id' => $tier->id, + 'payer_type' => 'groom', + ]); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + Event::assertDispatched(\App\Events\WeddingCelebration::class); + } + + public function test_can_claim_envelope() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(['jjb' => 0]); + /** @var \App\Models\User $groom */ + $groom = User::factory()->create(); + /** @var \App\Models\User $bride */ + $bride = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $groom->id, + 'partner_id' => $bride->id, + 'status' => 'married', + 'married_at' => now(), + ], $this->createLegacyMarriageData())); + + $tier = WeddingTier::forceCreate([ + 'tier' => 1, + 'name' => 'Basic Wedding', + 'icon' => 'basic', + 'amount' => 1000, + 'description' => 'A basic wedding', + 'is_active' => true, + ]); + + $ceremony = WeddingCeremony::forceCreate([ + 'marriage_id' => $marriage->id, + 'tier_id' => $tier->id, + 'total_amount' => 1000, + 'payer_type' => 'groom', + 'groom_amount' => 1000, + 'partner_amount' => 0, + 'ceremony_type' => 'immediate', + 'status' => 'active', + 'expires_at' => now()->addHour(), + ]); + + // Mock existing envelope claim waiting to be claimed + WeddingEnvelopeClaim::forceCreate([ + 'ceremony_id' => $ceremony->id, + 'user_id' => $user->id, + 'amount' => 100, + 'claimed' => false, + ]); + + $response = $this->actingAs($user)->postJson(route('wedding.claim', ['ceremony' => $ceremony->id])); + + $response->assertStatus(200); + $response->assertJson(['ok' => true]); + + $this->assertEquals(100, $user->fresh()->jjb); + } + + public function test_can_get_envelope_status() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $groom */ + $groom = User::factory()->create(); + /** @var \App\Models\User $bride */ + $bride = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $groom->id, + 'partner_id' => $bride->id, + 'status' => 'married', + 'married_at' => now(), + ], $this->createLegacyMarriageData())); + + $tier = WeddingTier::forceCreate([ + 'tier' => 1, + 'name' => 'Basic', + 'icon' => 'basic', + 'amount' => 1000, + 'description' => 'basic', + 'is_active' => true, + ]); + + $ceremony = WeddingCeremony::forceCreate([ + 'marriage_id' => $marriage->id, + 'tier_id' => $tier->id, + 'total_amount' => 1000, + 'payer_type' => 'groom', + 'groom_amount' => 1000, + 'partner_amount' => 0, + 'ceremony_type' => 'immediate', + 'status' => 'active', + 'expires_at' => now()->addHour(), + ]); + + WeddingEnvelopeClaim::forceCreate([ + 'ceremony_id' => $ceremony->id, + 'user_id' => $user->id, + 'amount' => 100, + 'claimed' => false, + ]); + + $response = $this->actingAs($user)->getJson(route('wedding.envelope-status', ['ceremony' => $ceremony->id])); + + $response->assertStatus(200); + $response->assertJson([ + 'has_envelope' => true, + 'amount' => 100, + ]); + } + + public function test_can_get_pending_envelopes() + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + /** @var \App\Models\User $groom */ + $groom = User::factory()->create(); + /** @var \App\Models\User $bride */ + $bride = User::factory()->create(); + + $marriage = Marriage::create(array_merge([ + 'user_id' => $groom->id, + 'partner_id' => $bride->id, + 'status' => 'married', + 'married_at' => now(), + ], $this->createLegacyMarriageData())); + + $tier = WeddingTier::forceCreate([ + 'tier' => 1, + 'name' => 'Basic', + 'icon' => 'basic', + 'amount' => 1000, + 'description' => 'basic', + 'is_active' => true, + ]); + + $ceremony = WeddingCeremony::forceCreate([ + 'marriage_id' => $marriage->id, + 'tier_id' => $tier->id, + 'total_amount' => 1000, + 'payer_type' => 'groom', + 'groom_amount' => 1000, + 'partner_amount' => 0, + 'ceremony_type' => 'immediate', + 'status' => 'active', + 'expires_at' => now()->addHour(), + ]); + + WeddingEnvelopeClaim::forceCreate([ + 'ceremony_id' => $ceremony->id, + 'user_id' => $user->id, + 'amount' => 100, + 'claimed' => false, + ]); + + $response = $this->actingAs($user)->getJson(route('wedding.pending-envelopes')); + + $response->assertStatus(200); + $response->assertJsonStructure(['envelopes']); + $this->assertCount(1, $response->json('envelopes')); + $this->assertEquals($ceremony->id, $response->json('envelopes.0.ceremony_id')); + } +}