withHeaders(['X-Api-Key' => config('app.api_secret_key')]); }); it('registers a new user and returns a token', function () { $this->postJson('/api/auth/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]) ->assertCreated() ->assertJsonStructure(['token', 'user' => ['id', 'name', 'email']]); }); it('returns 422 when register fields are missing', function () { $this->postJson('/api/auth/register') ->assertUnprocessable() ->assertJsonValidationErrors(['name', 'email', 'password']); }); it('returns 422 when email is already taken', function () { User::factory()->create(['email' => 'taken@example.com']); $this->postJson('/api/auth/register', [ 'name' => 'Another User', 'email' => 'taken@example.com', 'password' => 'password', 'password_confirmation' => 'password', ]) ->assertUnprocessable() ->assertJsonValidationErrors(['email']); }); it('logs in with valid credentials and returns a token', function () { $user = User::factory()->create(['password' => bcrypt('secret123')]); $this->postJson('/api/auth/login', [ 'email' => $user->email, 'password' => 'secret123', ]) ->assertOk() ->assertJsonStructure(['token', 'user']); }); it('returns 401 for invalid credentials', function () { User::factory()->create(['email' => 'user@example.com', 'password' => bcrypt('correct')]); $this->postJson('/api/auth/login', [ 'email' => 'user@example.com', 'password' => 'wrong', ])->assertUnauthorized(); }); it('returns the authenticated user on /me', function () { $user = User::factory()->create(); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('email', $user->email); }); it('reports subscription_cancelled=false for a user with no subscription', function () { $user = User::factory()->create(); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', false); }); it('reports subscription_cancelled=false for an active paid subscription', function () { $user = User::factory()->create(); $user->subscriptions()->create([ 'type' => 'default', 'stripe_id' => 'sub_active', 'stripe_status' => 'active', 'stripe_price' => 'price_plus_monthly', 'quantity' => 1, ]); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', false); }); it('reports subscription_cancelled=true once the subscription is set to end at period end', function () { $user = User::factory()->create(); $user->subscriptions()->create([ 'type' => 'default', 'stripe_id' => 'sub_cancelling', 'stripe_status' => 'active', 'stripe_price' => 'price_plus_monthly', 'quantity' => 1, 'ends_at' => now()->addDays(20), ]); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', true); }); it('exposes subscribed_at, cadence and renewal date for an active monthly subscription', function () { Plan::where('name', 'plus')->update([ 'stripe_price_id_monthly' => 'price_plus_monthly_test', 'stripe_price_id_annual' => 'price_plus_annual_test', ]); $user = User::factory()->create(); $subscribedAt = now()->subDays(10)->startOfSecond(); $renewalAt = now()->addDays(20)->startOfSecond(); $user->subscriptions()->create([ 'type' => 'default', 'stripe_id' => 'sub_monthly_active', 'stripe_status' => 'active', 'stripe_price' => 'price_plus_monthly_test', 'quantity' => 1, 'current_period_end' => $renewalAt, 'created_at' => $subscribedAt, 'updated_at' => $subscribedAt, ]); $response = $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', false) ->assertJsonPath('subscription_cadence', 'monthly'); expect($response->json('subscribed_at'))->toStartWith($subscribedAt->toDateString()); expect($response->json('subscription_expires_at'))->toStartWith($renewalAt->toDateString()); }); it('reports cadence as annual when the active price is the annual one', function () { Plan::where('name', 'pro')->update([ 'stripe_price_id_monthly' => 'price_pro_monthly_test', 'stripe_price_id_annual' => 'price_pro_annual_test', ]); $user = User::factory()->create(); $user->subscriptions()->create([ 'type' => 'default', 'stripe_id' => 'sub_annual_active', 'stripe_status' => 'active', 'stripe_price' => 'price_pro_annual_test', 'quantity' => 1, ]); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cadence', 'annual'); }); it('uses ends_at as the expiry date when subscription is cancelled', function () { $user = User::factory()->create(); $endsAt = now()->addDays(15)->startOfSecond(); $renewalAt = now()->addDays(30)->startOfSecond(); $user->subscriptions()->create([ 'type' => 'default', 'stripe_id' => 'sub_cancelling_with_period', 'stripe_status' => 'active', 'stripe_price' => 'price_plus_monthly', 'quantity' => 1, 'ends_at' => $endsAt, 'current_period_end' => $renewalAt, ]); $response = $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', true); expect($response->json('subscription_expires_at'))->toStartWith($endsAt->toDateString()); }); it('returns null subscription metadata for users with no subscription', function () { $user = User::factory()->create(); $this->actingAs($user, 'sanctum') ->getJson('/api/auth/me') ->assertOk() ->assertJsonPath('subscription_cancelled', false) ->assertJsonPath('subscription_cadence', null) ->assertJsonPath('subscribed_at', null) ->assertJsonPath('subscription_expires_at', null); }); it('logs out and revokes the token', function () { $user = User::factory()->create(); $token = $user->createToken('api')->plainTextToken; $this->withToken($token) ->postJson('/api/auth/logout') ->assertOk() ->assertJsonPath('message', 'Logged out.'); expect($user->tokens()->count())->toBe(0); }); it('returns 401 on protected routes without a token', function () { $this->getJson('/api/auth/me')->assertUnauthorized(); });