mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
* fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements --------- Co-authored-by: Robin Tuszik <mail@robin.gg>
237 lines
8.4 KiB
Ruby
237 lines
8.4 KiB
Ruby
# 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
|