use netrunner_control_plane::db::repository::{AppRepository, PgRepository}; use sqlx::{PgPool, Row}; use std::sync::Arc; // ===================================================================== // ТЕСТ 1: Создание пользователя и пополнение баланса // ===================================================================== // SQLx сам создаст чистую БД, сам накатит миграции и сам передаст сюда pool #[sqlx::test] async fn test_upsert_user_and_add_balance(pool: PgPool) { let repo = PgRepository::new(pool); let tg_id = 999111222; let user = repo .upsert_user(tg_id, Some("GhostRunner".into())) .await .unwrap(); assert_eq!(user.balance, 0); repo.add_balance(user.id, 5000).await.unwrap(); let updated_user = repo.get_user_by_id(user.id).await.unwrap().unwrap(); assert_eq!(updated_user.balance, 5000); } // ===================================================================== // ТЕСТ 2: Использование билетов (Cyberhack) // ===================================================================== #[sqlx::test] async fn test_consume_ticket_and_reward(pool: PgPool) { let repo = PgRepository::new(pool.clone()); let tg_id = 888777666; let user = repo.upsert_user(tg_id, None).await.unwrap(); assert_eq!(user.game_tickets, 0); let reward_amount = 300; let err = repo .consume_ticket_and_reward(user.id, reward_amount) .await .unwrap_err(); assert_eq!(err, "NO_TICKETS", "Нельзя играть без токенов доступа"); let no_change_user = repo.get_user_by_id(user.id).await.unwrap().unwrap(); assert_eq!(no_change_user.balance, 0); sqlx::query("UPDATE users SET game_tickets = 2 WHERE id = $1") .bind(user.id) .execute(&pool) .await .unwrap(); let success_now = repo .consume_ticket_and_reward(user.id, reward_amount) .await .unwrap(); assert!(success_now); let updated_user = repo.get_user_by_id(user.id).await.unwrap().unwrap(); assert_eq!(updated_user.game_tickets, 1); assert_eq!(updated_user.balance, reward_amount); } // ===================================================================== // ТЕ==== ТЕСТ 3: Стресс-тест промокодов (Проверка блокировки FOR UPDATE) // ===================================================================== #[sqlx::test] async fn test_apply_promo_code_race_condition(pool: PgPool) { let repo = Arc::new(PgRepository::new(pool.clone())); let promo_code = "CYBER_RACE_2077"; let reward_amount = 1500i64; sqlx::query( "INSERT INTO promo_codes (code, reward_amount, max_uses, current_uses) VALUES ($1, $2, 1, 0)", ) .bind(promo_code) .bind(reward_amount) .execute(&pool) .await .unwrap(); let num_requests = 10; let mut handles = vec![]; for i in 0..num_requests { let repo_clone = repo.clone(); let code = promo_code.to_string(); let handle = tokio::spawn(async move { let user = repo_clone.upsert_user(1000 + i, None).await.unwrap(); repo_clone.apply_promo_code(user.id, &code).await }); handles.push(handle); } let mut success_count = 0; let mut fail_count = 0; for handle in handles { let result = handle.await.unwrap(); match result { Ok(reward) => { assert_eq!(reward, reward_amount); success_count += 1; } Err(e) => { assert!(e == "CODE_DEPLETED" || e == "ALREADY_USED"); fail_count += 1; } } } assert_eq!( success_count, 1, "Блокировка FOR UPDATE должна пропустить только один поток" ); assert_eq!(fail_count, num_requests - 1); let promo_record = sqlx::query("SELECT current_uses FROM promo_codes WHERE code = $1") .bind(promo_code) .fetch_one(&pool) .await .unwrap(); let current_uses: i32 = promo_record.get("current_uses"); assert_eq!(current_uses, 1); }