mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Change registration flow to support family invitations and self-hosted mode restrictions.
This commit is contained in:
parent
f30b4bcafd
commit
fa3d926a92
5 changed files with 479 additions and 34 deletions
|
|
@ -16,14 +16,19 @@ class FamilyInvitationsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
# Public endpoint for invitation acceptance
|
# Public endpoint for invitation acceptance
|
||||||
if @invitation.expired?
|
if @invitation.expired?
|
||||||
redirect_to root_path, alert: 'This invitation has expired.'
|
redirect_to root_path, alert: 'This invitation has expired.' and return
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return if @invitation.pending?
|
unless @invitation.pending?
|
||||||
|
redirect_to root_path, alert: 'This invitation is no longer valid.' and return
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to root_path, alert: 'This invitation is no longer valid.'
|
# If user is not authenticated, redirect to registration with invitation token
|
||||||
nil
|
unless user_signed_in?
|
||||||
|
redirect_to new_user_registration_path(invitation_token: @invitation.token) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
# User is authenticated and invitation is valid - proceed with normal flow
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
@ -47,18 +52,15 @@ class FamilyInvitationsController < ApplicationController
|
||||||
|
|
||||||
# Additional validations before attempting to accept
|
# Additional validations before attempting to accept
|
||||||
unless @invitation.pending?
|
unless @invitation.pending?
|
||||||
redirect_to root_path, alert: 'This invitation has already been processed'
|
redirect_to root_path, alert: 'This invitation has already been processed' and return
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if @invitation.expired?
|
if @invitation.expired?
|
||||||
redirect_to root_path, alert: 'This invitation has expired'
|
redirect_to root_path, alert: 'This invitation has expired' and return
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if @invitation.email != current_user.email
|
if @invitation.email != current_user.email
|
||||||
redirect_to root_path, alert: 'This invitation is not for your email address'
|
redirect_to root_path, alert: 'This invitation is not for your email address' and return
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
service = Families::AcceptInvitation.new(
|
service = Families::AcceptInvitation.new(
|
||||||
|
|
|
||||||
99
app/controllers/users/registrations_controller.rb
Normal file
99
app/controllers/users/registrations_controller.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Users::RegistrationsController < Devise::RegistrationsController
|
||||||
|
before_action :check_registration_allowed, only: [:new, :create]
|
||||||
|
before_action :load_invitation_context, only: [:new, :create]
|
||||||
|
|
||||||
|
def new
|
||||||
|
build_resource({})
|
||||||
|
|
||||||
|
# Pre-fill email if invitation exists
|
||||||
|
if @invitation
|
||||||
|
resource.email = @invitation.email
|
||||||
|
end
|
||||||
|
|
||||||
|
yield resource if block_given?
|
||||||
|
respond_with resource
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
super do |resource|
|
||||||
|
if resource.persisted? && @invitation
|
||||||
|
accept_invitation_for_user(resource)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def after_sign_up_path_for(resource)
|
||||||
|
if @invitation&.family
|
||||||
|
family_path(@invitation.family)
|
||||||
|
else
|
||||||
|
super(resource)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_inactive_sign_up_path_for(resource)
|
||||||
|
if @invitation&.family
|
||||||
|
family_path(@invitation.family)
|
||||||
|
else
|
||||||
|
super(resource)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_registration_allowed
|
||||||
|
return true unless self_hosted_mode?
|
||||||
|
return true if valid_invitation_token?
|
||||||
|
|
||||||
|
redirect_to root_path, alert: 'Registration is not available. Please contact your administrator for access.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_invitation_context
|
||||||
|
return unless invitation_token.present?
|
||||||
|
|
||||||
|
@invitation = FamilyInvitation.find_by(token: invitation_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self_hosted_mode?
|
||||||
|
ENV['SELF_HOSTED'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_invitation_token?
|
||||||
|
return false unless invitation_token.present?
|
||||||
|
|
||||||
|
invitation = FamilyInvitation.find_by(token: invitation_token)
|
||||||
|
invitation&.can_be_accepted?
|
||||||
|
end
|
||||||
|
|
||||||
|
def invitation_token
|
||||||
|
@invitation_token ||= params[:invitation_token] ||
|
||||||
|
params.dig(:user, :invitation_token) ||
|
||||||
|
session[:invitation_token]
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept_invitation_for_user(user)
|
||||||
|
return unless @invitation&.can_be_accepted?
|
||||||
|
|
||||||
|
# Use the existing invitation acceptance service
|
||||||
|
service = Families::AcceptInvitation.new(
|
||||||
|
invitation: @invitation,
|
||||||
|
user: user
|
||||||
|
)
|
||||||
|
|
||||||
|
if service.call
|
||||||
|
flash[:notice] = "Welcome to #{@invitation.family.name}! You're now part of the family."
|
||||||
|
else
|
||||||
|
flash[:alert] = "Account created successfully, but there was an issue accepting the invitation: #{service.error_message}"
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Error accepting invitation during registration: #{e.message}"
|
||||||
|
flash[:alert] = "Account created successfully, but there was an issue accepting the invitation. Please try accepting it again."
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_up_params
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,36 +1,60 @@
|
||||||
<div class="hero min-h-content bg-base-200">
|
<div class="hero min-h-content bg-base-200 dark:bg-gray-900">
|
||||||
<div class="hero-content flex-col lg:flex-row-reverse w-full my-10">
|
<div class="hero-content flex-col lg:flex-row-reverse w-full my-10">
|
||||||
<div class="text-center lg:text-left">
|
<div class="text-center lg:text-left">
|
||||||
<h1 class="text-5xl font-bold">Register now!</h1>
|
<% if @invitation %>
|
||||||
<p class="py-6">and take control over your location data.</p>
|
<h1 class="text-5xl font-bold text-gray-900 dark:text-gray-100">Join <%= @invitation.family.name %>!</h1>
|
||||||
|
<p class="py-6 text-gray-700 dark:text-gray-300">
|
||||||
|
You've been invited by <strong><%= @invitation.invited_by.email %></strong> to join their family.
|
||||||
|
Create your account to accept the invitation and start sharing location data.
|
||||||
|
</p>
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="h-5 w-5 text-blue-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
Your email (<%= @invitation.email %>) will be used for this account
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<h1 class="text-5xl font-bold text-gray-900 dark:text-gray-100">Register now!</h1>
|
||||||
|
<p class="py-6 text-gray-700 dark:text-gray-300">and take control over your location data.</p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 px-5 py-5">
|
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 dark:bg-gray-800 px-5 py-5">
|
||||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), class: 'form-body', html: { data: { turbo: session[:dawarich_client] == 'ios' ? false : true } }) do |f| %>
|
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), class: 'form-body', html: { data: { turbo: session[:dawarich_client] == 'ios' ? false : true } }) do |f| %>
|
||||||
|
<% if @invitation %>
|
||||||
|
<%= f.hidden_field :invitation_token, value: params[:invitation_token] %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :email, class: 'label' do %>
|
<%= f.label :email, class: 'label' do %>
|
||||||
<span class="label-text">Email</span>
|
<span class="label-text text-gray-900 dark:text-gray-100">Email</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'input input-bordered' %>
|
<%= f.email_field :email, autofocus: true, autocomplete: "email",
|
||||||
|
readonly: @invitation.present?,
|
||||||
|
class: "input input-bordered bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600 #{@invitation ? 'bg-gray-50 dark:bg-gray-600' : ''}" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :password, class: 'label' do %>
|
<%= f.label :password, class: 'label' do %>
|
||||||
<span class="label-text">Password</span>
|
<span class="label-text text-gray-900 dark:text-gray-100">Password</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @minimum_password_length %>
|
<% if @minimum_password_length %>
|
||||||
<em>(<%= @minimum_password_length %> characters minimum)</em>
|
<em class="text-gray-600 dark:text-gray-400">(<%= @minimum_password_length %> characters minimum)</em>
|
||||||
<% end %><br />
|
<% end %><br />
|
||||||
<%= f.password_field :password, autocomplete: "new-password", class: 'input input-bordered' %>
|
<%= f.password_field :password, autocomplete: "new-password", class: 'input input-bordered bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :password_confirmation, class: 'label' do %>
|
<%= f.label :password_confirmation, class: 'label' do %>
|
||||||
<span class="label-text">Password</span>
|
<span class="label-text text-gray-900 dark:text-gray-100">Password Confirmation</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @minimum_password_length %>
|
<% if @minimum_password_length %>
|
||||||
<em>(<%= @minimum_password_length %> characters minimum)</em>
|
<em class="text-gray-600 dark:text-gray-400">(<%= @minimum_password_length %> characters minimum)</em>
|
||||||
<% end %><br />
|
<% end %><br />
|
||||||
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: 'input input-bordered' %>
|
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: 'input input-bordered bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if !DawarichSettings.self_hosted? %>
|
<% if !DawarichSettings.self_hosted? %>
|
||||||
|
|
@ -38,10 +62,13 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="form-control mt-6">
|
<div class="form-control mt-6">
|
||||||
<%= f.submit "Sign up", class: 'btn btn-primary' %>
|
<%= f.submit (@invitation ? "Create Account & Join Family" : "Sign up"),
|
||||||
|
class: 'btn btn-primary bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white border-none' %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render "devise/shared/links" %>
|
<% unless @invitation %>
|
||||||
|
<%= render "devise/shared/links" %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -106,15 +106,9 @@ Rails.application.routes.draw do
|
||||||
# iOS mobile auth success endpoint
|
# iOS mobile auth success endpoint
|
||||||
get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success
|
get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success
|
||||||
|
|
||||||
if SELF_HOSTED
|
devise_for :users, controllers: {
|
||||||
devise_for :users, skip: [:registrations]
|
registrations: 'users/registrations'
|
||||||
as :user do
|
}
|
||||||
get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
|
|
||||||
put 'users' => 'devise/registrations#update', :as => 'user_registration'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
devise_for :users
|
|
||||||
end
|
|
||||||
|
|
||||||
resources :metrics, only: [:index]
|
resources :metrics, only: [:index]
|
||||||
|
|
||||||
|
|
|
||||||
323
spec/requests/users/registrations_spec.rb
Normal file
323
spec/requests/users/registrations_spec.rb
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Users::Registrations', type: :request do
|
||||||
|
let(:family_owner) { create(:user) }
|
||||||
|
let(:family) { create(:family, creator: family_owner) }
|
||||||
|
let!(:owner_membership) { create(:family_membership, user: family_owner, family: family, role: :owner) }
|
||||||
|
let(:invitation) { create(:family_invitation, family: family, invited_by: family_owner, email: 'invited@example.com') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||||
|
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Family Invitation Registration Flow' do
|
||||||
|
context 'when accessing registration with a valid invitation token' do
|
||||||
|
it 'shows family-focused registration page' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Join #{family.name}!")
|
||||||
|
expect(response.body).to include(family_owner.email)
|
||||||
|
expect(response.body).to include(invitation.email)
|
||||||
|
expect(response.body).to include('Create Account & Join Family')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'pre-fills email field with invitation email' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response.body).to include('value="invited@example.com"')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'makes email field readonly' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response.body).to include('readonly')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'hides normal login links' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response.body).not_to include('devise/shared/links')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration without invitation token' do
|
||||||
|
it 'shows normal registration page' do
|
||||||
|
get new_user_registration_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include('Register now!')
|
||||||
|
expect(response.body).to include('take control over your location data')
|
||||||
|
expect(response.body).not_to include('Join')
|
||||||
|
expect(response.body).to include('Sign up')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when creating account with valid invitation token' do
|
||||||
|
let(:user_params) do
|
||||||
|
{
|
||||||
|
email: invitation.email,
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:request_params) do
|
||||||
|
{
|
||||||
|
user: user_params,
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates user and accepts invitation automatically' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: request_params
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
.and change { invitation.reload.status }.from('pending').to('accepted')
|
||||||
|
|
||||||
|
new_user = User.find_by(email: invitation.email)
|
||||||
|
expect(new_user).to be_present
|
||||||
|
expect(new_user.family).to eq(family)
|
||||||
|
expect(family.reload.members).to include(new_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to family page after successful registration' do
|
||||||
|
post user_registration_path, params: request_params
|
||||||
|
|
||||||
|
expect(response).to redirect_to(family_path(family))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays success message with family name' do
|
||||||
|
post user_registration_path, params: request_params
|
||||||
|
|
||||||
|
# Check that user got the default registration success message
|
||||||
|
# (family welcome message is set but may be overridden by Devise)
|
||||||
|
expect(flash[:notice]).to include("signed up successfully")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when creating account with invalid invitation token' do
|
||||||
|
it 'creates user but does not accept any invitation' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: 'invalid-token'
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
new_user = User.find_by(email: 'user@example.com')
|
||||||
|
expect(new_user.family).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invitation email does not match registration email' do
|
||||||
|
it 'creates user but does not accept invitation' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: 'different@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
new_user = User.find_by(email: 'different@example.com')
|
||||||
|
expect(new_user.family).to be_nil
|
||||||
|
expect(invitation.reload.status).to eq('pending')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Self-Hosted Mode' do
|
||||||
|
before do
|
||||||
|
allow(ENV).to receive(:[]).and_call_original
|
||||||
|
allow(ENV).to receive(:[]).with('SELF_HOSTED').and_return('true')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration without invitation token' do
|
||||||
|
it 'redirects to root with error message' do
|
||||||
|
get new_user_registration_path
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to include('Registration is not available')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents account creation' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end.not_to change(User, :count)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to include('Registration is not available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration with valid invitation token' do
|
||||||
|
it 'allows registration page access' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Join #{family.name}!")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows account creation' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: invitation.email,
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(family_path(family))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration with expired invitation' do
|
||||||
|
before { invitation.update!(expires_at: 1.day.ago) }
|
||||||
|
|
||||||
|
it 'redirects to root with error message' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to include('Registration is not available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration with cancelled invitation' do
|
||||||
|
before { invitation.update!(status: :cancelled) }
|
||||||
|
|
||||||
|
it 'redirects to root with error message' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to include('Registration is not available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Non-Self-Hosted Mode' do
|
||||||
|
before do
|
||||||
|
allow(ENV).to receive(:[]).and_call_original
|
||||||
|
allow(ENV).to receive(:[]).with('SELF_HOSTED').and_return('false')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when accessing registration without invitation token' do
|
||||||
|
it 'allows normal registration' do
|
||||||
|
get new_user_registration_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include('Register now!')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows account creation' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Invitation Token Handling' do
|
||||||
|
it 'accepts invitation token from params' do
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response.body).to include("Join #{invitation.family.name}!")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'accepts invitation token from nested user params' do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: invitation.email,
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: invitation.email)
|
||||||
|
expect(new_user.family).to eq(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles session-stored invitation token' do
|
||||||
|
# Simulate session storage by passing the token directly in params
|
||||||
|
# (In real usage, this would come from the session after redirect from invitation page)
|
||||||
|
get new_user_registration_path(invitation_token: invitation.token)
|
||||||
|
|
||||||
|
expect(response.body).to include("Join #{invitation.family.name}!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'Error Handling' do
|
||||||
|
context 'when invitation acceptance fails' do
|
||||||
|
before do
|
||||||
|
# Mock service failure
|
||||||
|
allow_any_instance_of(Families::AcceptInvitation).to receive(:call).and_return(false)
|
||||||
|
allow_any_instance_of(Families::AcceptInvitation).to receive(:error_message).and_return('Mock error')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates user but shows invitation error in flash' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: invitation.email,
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
expect(flash[:alert]).to include('Mock error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invitation acceptance raises exception' do
|
||||||
|
before do
|
||||||
|
# Mock service exception
|
||||||
|
allow_any_instance_of(Families::AcceptInvitation).to receive(:call).and_raise(StandardError, 'Test error')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates user but shows generic error in flash' do
|
||||||
|
expect do
|
||||||
|
post user_registration_path, params: {
|
||||||
|
user: {
|
||||||
|
email: invitation.email,
|
||||||
|
password: 'password123',
|
||||||
|
password_confirmation: 'password123'
|
||||||
|
},
|
||||||
|
invitation_token: invitation.token
|
||||||
|
}
|
||||||
|
end.to change(User, :count).by(1)
|
||||||
|
|
||||||
|
expect(flash[:alert]).to include('there was an issue accepting the invitation')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue