set('admob.rewarded.android_ad_unit_id', 'ca-app-pub-test/android-rewarded'); config()->set('admob.rewarded.ios_ad_unit_id', 'ca-app-pub-test/ios-rewarded'); config()->set('admob.rewarded.reward_amount', 1); config()->set('admob.rewarded.reward_item', 'generation_credit'); $this->app->instance( AdMobServerSideVerificationVerifier::class, new class extends AdMobServerSideVerificationVerifier { public function verify(Request $request): void { // } }, ); }); it('returns the generation credit status', function () { $user = User::factory()->create(); GenerationCreditBalance::factory()->create([ 'user_id' => $user->id, 'balance' => 2, ]); Sanctum::actingAs($user); $this ->getJson('/api/me/generation-credits') ->assertOk() ->assertJsonPath('data.balance', 2) ->assertJsonPath('data.canWatchRewardedAd', true) ->assertJsonPath('data.rewardedAdConfigured', true); }); it('creates a rewarded ad session for the current day', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $response = $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'android', ]) ->assertCreated() ->assertJsonPath('data.adUnitId', 'ca-app-pub-test/android-rewarded') ->assertJsonPath('data.userId', $user->id); $session = RewardedAdSession::query()->sole(); expect($session->custom_data)->toBe($response->json('data.customData')) ->and($session->status)->toBe(RewardedAdSessionStatus::CREATED) ->and($session->reward_date->toDateString())->toBe(now('UTC')->toDateString()); }); it('reuses an active rewarded ad session', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $firstSessionId = $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'ios', ]) ->assertCreated() ->json('data.id'); $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'ios', ]) ->assertOk() ->assertJsonPath('data.id', $firstSessionId); expect(RewardedAdSession::query()->count())->toBe(1); }); it('grants one generation credit from a verified rewarded ad callback', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $sessionPayload = $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'android', ]) ->assertCreated() ->json('data'); $this ->get('/api/admob/rewarded-ads/callback?'.http_build_query([ 'ad_network' => '5450213213286189855', 'ad_unit' => 'ca-app-pub-test/android-rewarded', 'custom_data' => $sessionPayload['customData'], 'reward_amount' => 1, 'reward_item' => 'generation_credit', 'timestamp' => now()->getTimestampMs(), 'transaction_id' => 'reward-transaction-1', 'user_id' => $user->id, ])) ->assertOk() ->assertSee('ok'); $transaction = GenerationCreditTransaction::query()->sole(); $session = RewardedAdSession::query()->sole(); expect($transaction->type)->toBe(GenerationCreditTransactionType::AD_REWARD) ->and($transaction->amount)->toBe(1) ->and($transaction->daily_reward_key)->toBe('ad:'.$session->reward_date->toDateString()) ->and($session->status)->toBe(RewardedAdSessionStatus::REWARDED) ->and(GenerationCreditBalance::query()->whereKey($user->id)->value('balance'))->toBe(1); }); it('accepts the numeric ad unit id returned by admob callbacks', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $sessionPayload = $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'android', ]) ->assertCreated() ->json('data'); $this ->get('/api/admob/rewarded-ads/callback?'.http_build_query([ 'ad_unit' => 'android-rewarded', 'custom_data' => $sessionPayload['customData'], 'reward_amount' => 1, 'reward_item' => 'generation_credit', 'transaction_id' => 'reward-transaction-numeric-ad-unit', 'user_id' => $user->id, ])) ->assertOk(); expect(GenerationCreditBalance::query()->whereKey($user->id)->value('balance'))->toBe(1); }); it('rejects unexpected rewarded ad units', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $sessionPayload = $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'android', ]) ->assertCreated() ->json('data'); $this ->get('/api/admob/rewarded-ads/callback?'.http_build_query([ 'ad_unit' => 'unexpected-ad-unit', 'custom_data' => $sessionPayload['customData'], 'reward_amount' => 1, 'reward_item' => 'generation_credit', 'transaction_id' => 'reward-transaction-unexpected-ad-unit', 'user_id' => $user->id, ])) ->assertBadRequest(); expect(GenerationCreditBalance::query()->whereKey($user->id)->value('balance'))->toBeNull(); }); it('does not grant more than one rewarded ad credit per day', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $firstSession = RewardedAdSession::factory()->create([ 'user_id' => $user->id, 'reward_date' => now('UTC')->toDateString(), ]); $secondSession = RewardedAdSession::factory()->create([ 'user_id' => $user->id, 'reward_date' => now('UTC')->toDateString(), ]); foreach ([$firstSession, $secondSession] as $index => $session) { $this ->get('/api/admob/rewarded-ads/callback?'.http_build_query([ 'ad_unit' => 'ca-app-pub-test/android-rewarded', 'custom_data' => $session->custom_data, 'reward_amount' => 1, 'reward_item' => 'generation_credit', 'transaction_id' => 'reward-transaction-'.$index, 'user_id' => $user->id, ])) ->assertOk(); } expect(GenerationCreditTransaction::query()->where('type', GenerationCreditTransactionType::AD_REWARD)->count())->toBe(1) ->and(GenerationCreditBalance::query()->whereKey($user->id)->value('balance'))->toBe(1) ->and($secondSession->refresh()->status)->toBe(RewardedAdSessionStatus::REJECTED); }); it('prevents new rewarded ad sessions after today reward is claimed', function () { $user = User::factory()->create(); GenerationCreditTransaction::factory()->create([ 'user_id' => $user->id, 'daily_reward_key' => 'ad:'.now('UTC')->toDateString(), ]); Sanctum::actingAs($user); $this ->postJson('/api/me/rewarded-ad-sessions', [ 'platform' => 'android', ]) ->assertUnprocessable() ->assertJsonPath('message', __('api.generation_credits.rewarded_ad_unavailable')); });