mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
* Implement OmniAuth GitHub authentication * Fix omniauth GitHub scope to include user email access * Remove margin-bottom * Implement Google OAuth2 authentication * Implement OIDC authentication for Dawarich using omniauth_openid_connect gem. * Add patreon account linking and patron checking service * Update docker-compose.yml to use boolean values instead of strings * Add support for KML files * Add tests * Update changelog * Remove patreon OAuth integration * Move omniauthable to a concern * Update an icon in integrations * Update changelog * Update app version * Fix family location sharing toggle * Move family location sharing to its own controller * Update changelog * Implement basic tagging functionality for places, allowing users to categorize and label places with custom tags. * Add places management API and tags feature * Add some changes related to places management feature * Fix some tests * Fix sometests * Add places layer * Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control * Rework tag form * Add hashtag * Add privacy zones to tags * Add notes to places and manage place tags * Update changelog * Update e2e tests * Extract tag serializer to its own file * Fix some tests * Fix tags request specs * Fix some tests * Fix rest of the tests * Revert some changes * Add missing specs * Revert changes in place export/import code * Fix some specs * Fix PlaceFinder to only consider global places when finding existing places * Fix few more specs * Fix visits creator spec * Fix last tests * Update place creating modal * Add home location based on "Home" tagged place * Save enabled tag layers * Some fixes * Fix bug where enabling place tag layers would trigger saving enabled layers, overwriting with incomplete data * Update migration to use disable_ddl_transaction! and add up/down methods * Fix tag layers restoration and filtering logic * Update OIDC auto-registration and email/password registration settings * Fix potential xss
295 lines
11 KiB
Ruby
295 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe 'Family Workflows', type: :request do
|
|
let(:user1) { create(:user, email: 'alice@example.com') }
|
|
let(:user2) { create(:user, email: 'bob@example.com') }
|
|
let(:user3) { create(:user, email: 'charlie@example.com') }
|
|
|
|
describe 'Complete family creation and management workflow' do
|
|
it 'allows creating a family, inviting members, and managing the family' do
|
|
# Step 1: User1 creates a family
|
|
sign_in user1
|
|
|
|
get '/family/new'
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
post '/family', params: { family: { name: 'The Smith Family' } }
|
|
|
|
# The redirect should be to the newly created family
|
|
expect(response).to have_http_status(:found)
|
|
family = Family.find_by(name: 'The Smith Family')
|
|
expect(family).to be_present
|
|
expect(family.name).to eq('The Smith Family')
|
|
expect(family.creator).to eq(user1)
|
|
expect(user1.reload.family).to eq(family)
|
|
expect(user1.family_owner?).to be true
|
|
|
|
# Step 2: User1 invites User2
|
|
post "/family/invitations", params: {
|
|
family_invitation: { email: user2.email }
|
|
}
|
|
expect(response).to redirect_to(family_path)
|
|
|
|
invitation = family.family_invitations.find_by(email: user2.email)
|
|
expect(invitation).to be_present
|
|
expect(invitation.email).to eq(user2.email)
|
|
expect(invitation.family).to eq(family)
|
|
expect(invitation.pending?).to be true
|
|
|
|
# Step 3: User2 views and accepts invitation
|
|
sign_out user1
|
|
|
|
# Public invitation view (no auth required)
|
|
get "/invitations/#{invitation.token}"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
# User2 accepts invitation
|
|
sign_in user2
|
|
post accept_family_invitation_path(token: invitation.token)
|
|
expect(response).to redirect_to(family_path)
|
|
|
|
expect(user2.reload.family).to eq(family)
|
|
expect(user2.family_owner?).to be false
|
|
expect(invitation.reload.accepted?).to be true
|
|
|
|
# Step 4: User1 invites User3
|
|
sign_in user1
|
|
post "/family/invitations", params: {
|
|
family_invitation: { email: user3.email }
|
|
}
|
|
|
|
invitation2 = family.family_invitations.find_by(email: user3.email)
|
|
expect(invitation2).to be_present
|
|
expect(invitation2.email).to eq(user3.email)
|
|
|
|
# Step 5: User3 accepts invitation
|
|
sign_in user3
|
|
post accept_family_invitation_path(token: invitation2.token)
|
|
|
|
expect(user3.reload.family).to eq(family)
|
|
expect(family.reload.members.count).to eq(3)
|
|
|
|
# Step 6: Family owner views members on family show page
|
|
sign_in user1
|
|
get "/family"
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
# Step 7: Owner removes a member
|
|
delete "/family/members/#{user2.family_membership.id}"
|
|
expect(response).to redirect_to(family_path)
|
|
|
|
expect(user2.reload.family).to be_nil
|
|
expect(family.reload.members.count).to eq(2)
|
|
expect(family.members).to include(user1, user3)
|
|
expect(family.members).not_to include(user2)
|
|
end
|
|
end
|
|
|
|
describe 'Family invitation expiration workflow' do
|
|
let(:family) { create(:family, name: 'Test Family', creator: user1) }
|
|
let!(:owner_membership) { create(:family_membership, user: user1, family: family, role: :owner) }
|
|
let!(:invitation) do
|
|
create(:family_invitation, family: family, email: user2.email, invited_by: user1, expires_at: 1.day.ago)
|
|
end
|
|
|
|
it 'handles expired invitations correctly' do
|
|
# User2 tries to view expired invitation
|
|
get "/invitations/#{invitation.token}"
|
|
expect(response).to redirect_to(root_path)
|
|
follow_redirect!
|
|
expect(response.body).to include('This invitation has expired')
|
|
|
|
# User2 tries to accept expired invitation
|
|
sign_in user2
|
|
post accept_family_invitation_path(token: invitation.token)
|
|
expect(response).to redirect_to(root_path)
|
|
|
|
expect(user2.reload.family).to be_nil
|
|
expect(invitation.reload.pending?).to be true
|
|
end
|
|
end
|
|
|
|
describe 'Multiple family membership prevention workflow' do
|
|
let(:family1) { create(:family, name: 'Family 1', creator: user1) }
|
|
let(:family2) { create(:family, name: 'Family 2', creator: user2) }
|
|
let!(:user1_membership) { create(:family_membership, user: user1, family: family1, role: :owner) }
|
|
let!(:user2_membership) { create(:family_membership, user: user2, family: family2, role: :owner) }
|
|
let!(:invitation1) { create(:family_invitation, family: family1, email: user3.email, invited_by: user1) }
|
|
let!(:invitation2) { create(:family_invitation, family: family2, email: user3.email, invited_by: user2) }
|
|
|
|
it 'prevents users from joining multiple families' do
|
|
# User3 accepts invitation to Family 1
|
|
sign_in user3
|
|
post accept_family_invitation_path(token: invitation1.token)
|
|
expect(response).to redirect_to(family_path)
|
|
expect(user3.family).to eq(family1)
|
|
|
|
# User3 tries to accept invitation to Family 2
|
|
post accept_family_invitation_path(token: invitation2.token)
|
|
expect(response).to redirect_to(root_path)
|
|
expect(flash[:alert]).to include('You must leave your current family')
|
|
|
|
expect(user3.reload.family).to eq(family1) # Still in first family
|
|
end
|
|
end
|
|
|
|
describe 'Family ownership transfer and leaving workflow' do
|
|
let(:family) { create(:family, creator: user1) }
|
|
let!(:owner_membership) { create(:family_membership, user: user1, family: family, role: :owner) }
|
|
let!(:member_membership) { create(:family_membership, user: user2, family: family, role: :member) }
|
|
|
|
it 'prevents owner from leaving when members exist' do
|
|
sign_in user1
|
|
|
|
# Owner tries to leave family with members (using memberships destroy route)
|
|
owner_membership = user1.family_membership
|
|
delete "/family/members/#{owner_membership.id}"
|
|
expect(response).to redirect_to(family_path)
|
|
follow_redirect!
|
|
expect(response.body).to include('cannot remove their own membership')
|
|
|
|
expect(user1.reload.family).to eq(family)
|
|
expect(user1.family_owner?).to be true
|
|
end
|
|
|
|
it 'allows owner to leave when they are the only member' do
|
|
sign_in user1
|
|
|
|
# Remove the member first
|
|
delete "/family/members/#{member_membership.id}"
|
|
|
|
# Owner cannot leave even when alone - they must delete the family instead
|
|
owner_membership = user1.reload.family_membership
|
|
delete "/family/members/#{owner_membership.id}"
|
|
expect(response).to redirect_to(family_path)
|
|
follow_redirect!
|
|
expect(response.body).to include('cannot remove their own membership')
|
|
|
|
expect(user1.reload.family).to eq(family)
|
|
end
|
|
|
|
it 'allows members to leave freely' do
|
|
sign_in user2
|
|
|
|
delete "/family/members/#{member_membership.id}"
|
|
expect(response).to redirect_to(new_family_path)
|
|
|
|
expect(user2.reload.family).to be_nil
|
|
expect(family.reload.members.count).to eq(1)
|
|
expect(family.members).to include(user1)
|
|
expect(family.members).not_to include(user2)
|
|
end
|
|
end
|
|
|
|
describe 'Family deletion workflow' do
|
|
let(:family) { create(:family, creator: user1) }
|
|
let!(:owner_membership) { create(:family_membership, user: user1, family: family, role: :owner) }
|
|
|
|
context 'when members exist' do
|
|
let!(:member_membership) { create(:family_membership, user: user2, family: family, role: :member) }
|
|
|
|
it 'prevents family deletion when members exist' do
|
|
sign_in user1
|
|
|
|
expect do
|
|
delete "/family"
|
|
end.not_to change(Family, :count)
|
|
|
|
expect(response).to redirect_to(family_path)
|
|
follow_redirect!
|
|
expect(response.body).to include('Cannot delete family with members')
|
|
end
|
|
end
|
|
|
|
it 'allows family deletion when owner is the only member' do
|
|
sign_in user1
|
|
|
|
expect do
|
|
delete "/family"
|
|
end.to change(Family, :count).by(-1)
|
|
|
|
expect(response).to redirect_to(new_family_path)
|
|
expect(user1.reload.family).to be_nil
|
|
end
|
|
end
|
|
|
|
describe 'Authorization workflow' do
|
|
let(:family) { create(:family, creator: user1) }
|
|
let!(:owner_membership) { create(:family_membership, user: user1, family: family, role: :owner) }
|
|
let!(:member_membership) { create(:family_membership, user: user2, family: family, role: :member) }
|
|
|
|
it 'enforces proper authorization for family management' do
|
|
# Member cannot invite others
|
|
sign_in user2
|
|
post "/family/invitations", params: {
|
|
family_invitation: { email: user3.email }
|
|
}
|
|
expect(response).to have_http_status(:see_other)
|
|
expect(flash[:alert]).to include('not authorized')
|
|
|
|
# Member cannot remove other members
|
|
delete "/family/members/#{owner_membership.id}"
|
|
expect(response).to have_http_status(:see_other)
|
|
expect(flash[:alert]).to include('not authorized')
|
|
|
|
# Member cannot edit family
|
|
patch "/family", params: { family: { name: 'Hacked Family' } }
|
|
expect(response).to have_http_status(:see_other)
|
|
expect(flash[:alert]).to include('not authorized')
|
|
|
|
# Member cannot delete family
|
|
delete "/family"
|
|
expect(response).to have_http_status(:see_other)
|
|
expect(flash[:alert]).to include('not authorized')
|
|
|
|
# Outsider cannot access family
|
|
sign_in user3
|
|
get "/family"
|
|
expect(response).to redirect_to(new_family_path)
|
|
end
|
|
end
|
|
|
|
describe 'Email invitation workflow' do
|
|
let(:family) { create(:family, name: 'Test Family', creator: user1) }
|
|
let!(:owner_membership) { create(:family_membership, user: user1, family: family, role: :owner) }
|
|
|
|
it 'handles invitation emails correctly' do
|
|
sign_in user1
|
|
|
|
# Mock email delivery
|
|
expect do
|
|
post "/family/invitations", params: {
|
|
family_invitation: { email: 'newuser@example.com' }
|
|
}
|
|
end.to change(Family::Invitation, :count).by(1)
|
|
|
|
invitation = family.family_invitations.find_by(email: 'newuser@example.com')
|
|
expect(invitation.email).to eq('newuser@example.com')
|
|
expect(invitation.token).to be_present
|
|
expect(invitation.expires_at).to be > Time.current
|
|
end
|
|
end
|
|
|
|
describe 'Navigation and redirect workflow' do
|
|
it 'handles proper redirects for family-related navigation' do
|
|
# User without family can access new family page
|
|
sign_in user1
|
|
get '/family/new'
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
# User creates family
|
|
post '/family', params: { family: { name: 'Test Family' } }
|
|
expect(response).to have_http_status(:found)
|
|
|
|
# User with family can view their family
|
|
get '/family'
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
# User with family gets redirected from new family page
|
|
get '/family/new'
|
|
expect(response).to redirect_to(family_path)
|
|
end
|
|
end
|
|
end
|