# frozen_string_literal: true require 'rails_helper' RSpec.describe 'Authentication', type: :request do let(:user) { create(:user, password: 'password123') } describe 'Route Protection' do it 'redirects to sign in page when accessing protected routes while signed out' do get map_v1_path expect(response).to redirect_to(new_user_session_path) end it 'allows access to protected routes when signed in' do sign_in user get map_path expect(response).to be_successful end end describe 'Account Management' do it 'prevents account update without current password' do sign_in user put user_registration_path, params: { user: { email: 'updated@example.com', current_password: '' } } expect(response).not_to be_successful expect(user.reload.email).not_to eq('updated@example.com') end it 'allows account update with current password' do sign_in user put user_registration_path, params: { user: { email: 'updated@example.com', current_password: 'password123' } } expect(response).to redirect_to(root_path) expect(user.reload.email).to eq('updated@example.com') end end describe 'Session Security' do it 'requires authentication after sign out' do sign_in user get map_path expect(response).to be_successful sign_out user get map_path expect(response).to redirect_to(new_user_session_path) end end describe 'Mobile iOS Authentication' do it 'redirects to iOS success path when signing in with iOS client header' do # Make a login request with the iOS client header (user NOT pre-signed in) post user_session_path, params: { user: { email: user.email, password: 'password123' } }, headers: { 'X-Dawarich-Client' => 'ios', 'Accept' => 'text/html' } # Should redirect to iOS success endpoint after successful login # The redirect will include a token parameter generated by after_sign_in_path_for expect(response).to redirect_to(%r{auth/ios/success\?token=}) expect(response.location).to include('token=') end it 'stores iOS client header in session' do # Test that the header gets stored when accessing sign-in page get new_user_session_path, headers: { 'X-Dawarich-Client' => 'ios' } expect(session[:dawarich_client]).to eq('ios') end it 'redirects to iOS success path using stored session value' do # Simulate iOS app accessing sign-in page first (stores header in session) get new_user_session_path, headers: { 'X-Dawarich-Client' => 'ios' } # Then sign-in POST request without header (relies on session) post user_session_path, params: { user: { email: user.email, password: 'password123' } }, headers: { 'Accept' => 'text/html' } # Should still redirect to iOS success endpoint using session value expect(response).to redirect_to(%r{auth/ios/success\?token=}) expect(response.location).to include('token=') end it 'returns plain text response for iOS success endpoint with token' do # Generate a test JWT token using the same service as the controller payload = { api_key: user.api_key, exp: 5.minutes.from_now.to_i } test_token = Subscription::EncodeJwtToken.new( payload, ENV['AUTH_JWT_SECRET_KEY'] ).call get ios_success_path, params: { token: test_token } expect(response).to be_successful expect(response.content_type).to include('text/plain') expect(response.body).to eq('Authentication successful! You can close this window.') end it 'returns JSON response when no token is provided to iOS success endpoint' do get ios_success_path expect(response).to be_successful expect(response.content_type).to include('application/json') json_response = JSON.parse(response.body) expect(json_response['success']).to be true expect(json_response['message']).to eq('iOS authentication successful') expect(json_response['redirect_url']).to eq(root_url) end it 'generates JWT token with correct payload for iOS authentication' do # Test JWT token generation directly using the same logic as after_sign_in_path_for payload = { api_key: user.api_key, exp: 5.minutes.from_now.to_i } # Create JWT token using the same service token = Subscription::EncodeJwtToken.new( payload, ENV['AUTH_JWT_SECRET_KEY'] ).call expect(token).to be_present # Decode the token to verify the payload decoded_payload = JWT.decode( token, ENV['AUTH_JWT_SECRET_KEY'], true, { algorithm: 'HS256' } ).first expect(decoded_payload['api_key']).to eq(user.api_key) expect(decoded_payload['exp']).to be_present end it 'uses default path for non-iOS clients' do # Make a login request without iOS client header (user NOT pre-signed in) post user_session_path, params: { user: { email: user.email, password: 'password123' } } # Should redirect to default path (not iOS success) expect(response).not_to redirect_to(%r{auth/ios/success}) expect(response.location).not_to include('auth/ios/success') end end describe 'Family Invitation with Authentication' do let(:family) { create(:family, creator: user) } let!(:membership) { create(:family_membership, user: user, family: family, role: :owner) } let(:invitee) { create(:user, email: 'invitee@example.com', password: 'password123') } let(:invitation) { create(:family_invitation, family: family, invited_by: user, email: invitee.email) } it 'redirects to invitation page when signing in with invitation token in params' do post user_session_path, params: { user: { email: invitee.email, password: 'password123' }, invitation_token: invitation.token } expect(response).to redirect_to(family_invitation_path(invitation.token)) end it 'redirects to invitation page when signing in with invitation token in session' do # The invitation token is stored in session by Users::SessionsController#load_invitation_context # when accessing the sign-in page with invitation_token param get new_user_session_path, params: { invitation_token: invitation.token } # Then sign in without the invitation_token in params (should use session value) post user_session_path, params: { user: { email: invitee.email, password: 'password123' } } expect(response).to redirect_to(family_invitation_path(invitation.token)) end it 'prioritizes invitation over iOS flow when both are present' do # Sign in with both iOS header AND invitation token post user_session_path, params: { user: { email: invitee.email, password: 'password123' }, invitation_token: invitation.token }, headers: { 'X-Dawarich-Client' => 'ios' } # Should redirect to invitation page, NOT iOS success expect(response).to redirect_to(family_invitation_path(invitation.token)) expect(response.location).not_to include('auth/ios/success') end it 'redirects to iOS success when invitation is expired' do # Create an expired invitation expired_invitation = create(:family_invitation, family: family, invited_by: user, email: invitee.email, expires_at: 1.day.ago) # Sign in with iOS header and expired invitation token post user_session_path, params: { user: { email: invitee.email, password: 'password123' }, invitation_token: expired_invitation.token }, headers: { 'X-Dawarich-Client' => 'ios' } # Should redirect to iOS success since invitation can't be accepted expect(response).to redirect_to(%r{auth/ios/success\?token=}) end it 'uses default path when invitation token is invalid' do # Sign in with invalid invitation token post user_session_path, params: { user: { email: invitee.email, password: 'password123' }, invitation_token: 'invalid-token-123' } # Should use default redirect path expect(response).not_to redirect_to(%r{/invitations/}) expect(response).to redirect_to(root_path) end end end