测试: 完成游戏娱乐模块 (Gomoku, HorseRace, Lottery 等) 功能全量联调测试与代码格式化

This commit is contained in:
2026-04-03 13:55:36 +08:00
parent d47f9c5360
commit 659e562208
33 changed files with 3907 additions and 28 deletions

View File

@@ -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 验证通过
// 检测是否被封禁 (后台管理员级别获得豁免权,防止误把自己关在门外)

View File

@@ -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'],
];
}

View File

@@ -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'],
];
}

View File

@@ -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.');

View File

@@ -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,
]);
}
}

View File

@@ -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()],
]);
// ── 购买记录表 ────────────────────────────────────────────────

View File

@@ -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');
});
}

View File

@@ -23,8 +23,8 @@
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_DATABASE" value="chatroom_testing"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>

View File

@@ -0,0 +1,184 @@
<?php
namespace Tests\Feature;
use App\Models\Sysparam;
use App\Models\User;
use App\Models\UsernameBlacklist;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
class AuthControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Mock captcha validation to always pass
Validator::extend('captcha', function () {
return true;
});
// Mock Sysparam to return a default superlevel
Sysparam::updateOrCreate(
['alias' => '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();
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace Tests\Feature;
use App\Models\BaccaratBet;
use App\Models\BaccaratRound;
use App\Models\GameConfig;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class BaccaratControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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'));
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Tests\Feature;
use App\Models\BankLog;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BankControllerTest extends TestCase
{
use RefreshDatabase;
public function test_info_returns_balances_and_logs()
{
$user = User::factory()->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']);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Tests\Feature;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\AiChatService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Mockery\MockInterface;
use Tests\TestCase;
class ChatBotControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Redis::flushall();
parent::tearDown();
}
public function test_chatbot_disabled_by_default()
{
$user = User::factory()->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']);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Tests\Feature;
use App\Models\Room;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class ChatControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Redis::flushall();
}
public function test_can_view_room()
{
$room = Room::create(['room_name' => '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);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Tests\Feature;
use App\Models\Department;
use App\Models\Position;
use App\Models\PositionAuthorityLog;
use App\Models\PositionDutyLog;
use App\Models\User;
use App\Models\UserPosition;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DutyHallControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_view_duty_hall_roster_tab()
{
$user = User::factory()->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);
}
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class EarnControllerTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Redis::flushall(); // Clear redis keys so they don't leak between tests
parent::tearDown();
}
public function test_can_claim_video_reward()
{
$user = User::factory()->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,
]);
}
}

View File

@@ -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.
*/

View File

@@ -0,0 +1,125 @@
<?php
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class FishingControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Redis::flushall();
GameConfig::updateOrCreate(
['game_key' => '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',
]);
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Tests\Feature;
use App\Models\FortuneLog;
use App\Models\GameConfig;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FortuneTellingControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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'));
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Tests\Feature;
use App\Models\FriendRequest;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FriendControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_check_friend_status()
{
$me = User::factory()->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']);
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\GomokuGame;
use App\Models\Room;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class GomokuControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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,
]);
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Tests\Feature;
use App\Models\Guestbook;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class GuestbookControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_displays_public_messages()
{
$user = User::factory()->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]);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Tests\Feature;
use App\Models\HolidayClaim;
use App\Models\HolidayEvent;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HolidayControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_check_holiday_status()
{
/** @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',
]);
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' => '活动已结束或已过期。']);
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\HorseBet;
use App\Models\HorseRace;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class HorseRaceControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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'));
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class InviteControllerTest extends TestCase
{
use RefreshDatabase;
public function test_handle_sets_cookie()
{
$inviter = User::factory()->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');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Tests\Feature;
use App\Models\Sysparam;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LeaderboardControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_view_leaderboard_index()
{
$user = User::factory()->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');
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\LotteryIssue;
use App\Models\LotteryTicket;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LotteryControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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'));
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace Tests\Feature;
use App\Models\Marriage;
use App\Models\ShopItem;
use App\Models\User;
use App\Models\UserPurchase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class MarriageControllerTest extends TestCase
{
use RefreshDatabase;
private function createLegacyMarriageData()
{
return [
'hyname' => '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);
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Tests\Feature;
use App\Models\RedPacketEnvelope;
use App\Models\Sysparam;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class RedPacketControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Redis::flushall();
Sysparam::updateOrCreate(['alias' => '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,
]);
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Tests\Feature;
use App\Models\Room;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RoomControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_view_rooms_index()
{
$user = User::factory()->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,
]);
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Tests\Feature;
use App\Models\ShopItem;
use App\Models\User;
use App\Models\UserPurchase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ShopControllerTest extends TestCase
{
use RefreshDatabase;
public function test_items_returns_active_shop_items()
{
$user = User::factory()->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',
]);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\SlotMachineLog;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class SlotMachineControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
GameConfig::updateOrCreate(
['game_key' => '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'));
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace Tests\Feature;
use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Sysparam::updateOrCreate(['alias' => '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);
}
}

View File

@@ -0,0 +1,244 @@
<?php
namespace Tests\Feature;
use App\Models\Marriage;
use App\Models\User;
use App\Models\WeddingCeremony;
use App\Models\WeddingEnvelopeClaim;
use App\Models\WeddingTier;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class WeddingControllerTest extends TestCase
{
use RefreshDatabase;
private function createLegacyMarriageData()
{
return [
'hyname' => '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'));
}
}