Refactor: Apply Rails best practices to trip sharing implementation

- Remove unused @is_public_view variable from controller
- Simplify conditionals by leveraging methods that return [] when empty
- Move public view from trips/public_show to shared/trips/show (Rails conventions)
- Refactor trips#update to be HTML-only (remove JSON responses)
- Convert sharing form to use proper Rails form helpers
- Move JS controller to shared/ subdirectory with proper namespacing
- Create RSpec shared examples for Shareable concern to eliminate duplication
- Update request specs to match HTML-only controller behavior
- Apply 'render/redirect ... and return' pattern for early returns
This commit is contained in:
Claude 2025-11-07 12:05:34 +00:00
parent 429f90e666
commit b1cbb5555f
No known key found for this signature in database
10 changed files with 263 additions and 635 deletions

View file

@ -7,11 +7,8 @@ class Shared::TripsController < ApplicationController
redirect_to root_path, alert: 'Shared trip not found or no longer available' and return unless @trip&.public_accessible?
@user = @trip.user
@is_public_view = true
@coordinates = @trip.path.present? ? extract_coordinates : []
@photo_previews = @trip.share_photos? ? fetch_photo_previews : []
render 'trips/public_show'
@coordinates = extract_coordinates
@photo_previews = fetch_photo_previews
end
private
@ -24,6 +21,8 @@ class Shared::TripsController < ApplicationController
end
def fetch_photo_previews
return [] unless @trip.share_photos?
Rails.cache.fetch("trip_photos_#{@trip.id}", expires_in: 1.day) do
@trip.photo_previews
end

View file

@ -39,8 +39,8 @@ class TripsController < ApplicationController
end
def update
# Handle sharing settings update (JSON response)
update_sharing and return if params[:sharing]
# Handle sharing settings update
handle_sharing_update if params[:sharing]
# Handle regular trip update
if @trip.update(trip_params)
@ -68,38 +68,18 @@ class TripsController < ApplicationController
).map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7.to_s, _8.to_s] }
end
def update_sharing
def handle_sharing_update
if params[:sharing][:enabled] == '1'
sharing_options = {
expiration: params[:sharing][:expiration] || '24h'
expiration: params[:sharing][:expiration] || '24h',
share_notes: params[:sharing][:share_notes] == '1',
share_photos: params[:sharing][:share_photos] == '1'
}
# Add optional sharing settings
sharing_options[:share_notes] = params[:sharing][:share_notes] == '1'
sharing_options[:share_photos] = params[:sharing][:share_photos] == '1'
@trip.enable_sharing!(**sharing_options)
sharing_url = shared_trip_url(@trip.sharing_uuid)
render json: {
success: true,
sharing_url: sharing_url,
message: 'Sharing enabled successfully'
}
else
@trip.disable_sharing!
render json: {
success: true,
message: 'Sharing disabled successfully'
}
end
rescue StandardError => e
render json: {
success: false,
message: 'Failed to update sharing settings',
error: e.message
}, status: :unprocessable_content
end
def trip_params

View file

@ -1,5 +1,5 @@
import L from "leaflet";
import BaseController from "./base_controller";
import BaseController from "../base_controller";
export default class extends BaseController {
static values = {

View file

@ -54,9 +54,9 @@
<% if @coordinates.present? %>
<div id="public-trip-map"
class="w-full h-full"
data-controller="public-trip-map"
data-public-trip-map-coordinates-value="<%= @coordinates.to_json %>"
data-public-trip-map-name-value="<%= @trip.name %>"></div>
data-controller="shared--trip-map"
data-shared--trip-map-coordinates-value="<%= @coordinates.to_json %>"
data-shared--trip-map-name-value="<%= @trip.name %>"></div>
<% else %>
<div class="flex items-center justify-center h-full">
<div class="text-center">

View file

@ -5,7 +5,7 @@
Generate a public link to share this trip with others.
</p>
<div id="sharing-controls-<%= trip.id %>" data-trip-id="<%= trip.id %>">
<%= form_with model: trip, url: trip_path(trip), method: :patch, data: { turbo: false } do |f| %>
<% if trip.sharing_enabled? %>
<!-- Sharing is enabled -->
<div class="space-y-4">
@ -73,16 +73,11 @@
<!-- Action Buttons -->
<div class="flex gap-2">
<button
class="btn btn-warning flex-1"
onclick="regenerateTripSharingLink(<%= trip.id %>)">
Regenerate Link
</button>
<button
class="btn btn-error flex-1"
onclick="disableTripSharing(<%= trip.id %>)">
Disable Sharing
</button>
<%= f.submit "Disable Sharing",
name: 'sharing[enabled]',
value: '0',
class: "btn btn-error flex-1",
data: { turbo_confirm: "Are you sure you want to disable sharing for this trip?" } %>
</div>
</div>
<% else %>
@ -92,118 +87,40 @@
<label class="label">
<span class="label-text">Expiration</span>
</label>
<select class="select select-bordered" id="expiration-<%= trip.id %>">
<option value="1h">1 hour</option>
<option value="12h">12 hours</option>
<option value="24h" selected>24 hours</option>
<option value="permanent">Never (permanent)</option>
</select>
<%= f.select 'sharing[expiration]',
options_for_select([
['1 hour', '1h'],
['12 hours', '12h'],
['24 hours', '24h'],
['Never (permanent)', 'permanent']
], '24h'),
{},
class: 'select select-bordered' %>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Share notes</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="share-notes-<%= trip.id %>"
checked>
<%= f.check_box 'sharing[share_notes]',
{ checked: true, class: 'toggle toggle-primary' },
'1', '0' %>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Share photos</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="share-photos-<%= trip.id %>"
checked>
<%= f.check_box 'sharing[share_photos]',
{ checked: true, class: 'toggle toggle-primary' },
'1', '0' %>
</label>
</div>
</div>
<button
class="btn btn-primary w-full"
onclick="enableTripSharing(<%= trip.id %>)">
Enable Sharing
</button>
<%= f.hidden_field 'sharing[enabled]', value: '1' %>
<%= f.submit "Enable Sharing", class: "btn btn-primary w-full" %>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<script>
function enableTripSharing(tripId) {
const expiration = document.getElementById(`expiration-${tripId}`).value;
const shareNotes = document.getElementById(`share-notes-${tripId}`).checked ? '1' : '0';
const sharePhotos = document.getElementById(`share-photos-${tripId}`).checked ? '1' : '0';
fetch(`/trips/${tripId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
sharing: {
enabled: '1',
expiration: expiration,
share_notes: shareNotes,
share_photos: sharePhotos
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Failed to enable sharing: ' + (data.message || 'Unknown error'));
}
})
.catch(error => {
alert('Error enabling sharing: ' + error);
});
}
function disableTripSharing(tripId) {
if (!confirm('Are you sure you want to disable sharing for this trip?')) {
return;
}
fetch(`/trips/${tripId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
sharing: {
enabled: '0'
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Failed to disable sharing: ' + (data.message || 'Unknown error'));
}
})
.catch(error => {
alert('Error disabling sharing: ' + error);
});
}
function regenerateTripSharingLink(tripId) {
if (!confirm('Regenerating will invalidate the old link. Continue?')) {
return;
}
// This would require a new endpoint or modify the existing one
alert('Regenerate link functionality to be implemented');
}
</script>

View file

@ -1,213 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Shareable do
let(:user) { create(:user) }
let(:trip) do
create(:trip, user: user, name: 'Test Trip',
started_at: 1.week.ago, ended_at: Time.current)
end
describe '#generate_sharing_uuid' do
it 'generates a UUID before create' do
new_trip = build(:trip, user: user)
expect(new_trip.sharing_uuid).to be_nil
new_trip.save!
expect(new_trip.sharing_uuid).to be_present
expect(new_trip.sharing_uuid).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
end
end
describe '#sharing_enabled?' do
it 'returns false by default' do
expect(trip.sharing_enabled?).to be false
end
it 'returns true when enabled' do
trip.update!(sharing_settings: { 'enabled' => true })
expect(trip.sharing_enabled?).to be true
end
it 'returns false when disabled' do
trip.update!(sharing_settings: { 'enabled' => false })
expect(trip.sharing_enabled?).to be false
end
end
describe '#sharing_expired?' do
it 'returns false when no expiration is set' do
expect(trip.sharing_expired?).to be false
end
it 'returns false when expires_at is in the future' do
trip.update!(sharing_settings: {
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(trip.sharing_expired?).to be false
end
it 'returns true when expires_at is in the past' do
trip.update!(sharing_settings: {
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
expect(trip.sharing_expired?).to be true
end
it 'returns true when expiration is set but expires_at is missing' do
trip.update!(sharing_settings: {
'expiration' => '24h',
'expires_at' => nil
})
expect(trip.sharing_expired?).to be true
end
end
describe '#public_accessible?' do
it 'returns false by default' do
expect(trip.public_accessible?).to be false
end
it 'returns true when enabled and not expired' do
trip.update!(sharing_settings: {
'enabled' => true,
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(trip.public_accessible?).to be true
end
it 'returns false when enabled but expired' do
trip.update!(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
expect(trip.public_accessible?).to be false
end
it 'returns false when disabled' do
trip.update!(sharing_settings: {
'enabled' => false,
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(trip.public_accessible?).to be false
end
end
describe '#enable_sharing!' do
it 'enables sharing with default 24h expiration' do
trip.enable_sharing!
expect(trip.sharing_enabled?).to be true
expect(trip.sharing_settings['expiration']).to eq('24h')
expect(trip.sharing_settings['expires_at']).to be_present
end
it 'enables sharing with custom expiration' do
trip.enable_sharing!(expiration: '1h')
expect(trip.sharing_enabled?).to be true
expect(trip.sharing_settings['expiration']).to eq('1h')
end
it 'enables sharing with permanent expiration' do
trip.enable_sharing!(expiration: 'permanent')
expect(trip.sharing_enabled?).to be true
expect(trip.sharing_settings['expiration']).to eq('permanent')
expect(trip.sharing_settings['expires_at']).to be_nil
end
it 'defaults to 24h for invalid expiration' do
trip.enable_sharing!(expiration: 'invalid')
expect(trip.sharing_settings['expiration']).to eq('24h')
end
it 'stores additional options like share_notes' do
trip.enable_sharing!(expiration: '24h', share_notes: true)
expect(trip.sharing_settings['share_notes']).to be true
end
it 'stores additional options like share_photos' do
trip.enable_sharing!(expiration: '24h', share_photos: true)
expect(trip.sharing_settings['share_photos']).to be true
end
it 'generates a sharing_uuid if not present' do
trip.update_column(:sharing_uuid, nil)
trip.enable_sharing!
expect(trip.sharing_uuid).to be_present
end
it 'keeps existing sharing_uuid' do
original_uuid = trip.sharing_uuid
trip.enable_sharing!
expect(trip.sharing_uuid).to eq(original_uuid)
end
end
describe '#disable_sharing!' do
before do
trip.enable_sharing!(expiration: '24h')
end
it 'disables sharing' do
trip.disable_sharing!
expect(trip.sharing_enabled?).to be false
end
it 'clears expiration settings' do
trip.disable_sharing!
expect(trip.sharing_settings['expiration']).to be_nil
expect(trip.sharing_settings['expires_at']).to be_nil
end
it 'keeps the sharing_uuid' do
original_uuid = trip.sharing_uuid
trip.disable_sharing!
expect(trip.sharing_uuid).to eq(original_uuid)
end
end
describe '#generate_new_sharing_uuid!' do
it 'generates a new UUID' do
original_uuid = trip.sharing_uuid
trip.generate_new_sharing_uuid!
expect(trip.sharing_uuid).not_to eq(original_uuid)
expect(trip.sharing_uuid).to be_present
end
end
describe '#share_notes?' do
it 'returns false by default' do
expect(trip.share_notes?).to be false
end
it 'returns true when share_notes is enabled' do
trip.update!(sharing_settings: { 'share_notes' => true })
expect(trip.share_notes?).to be true
end
it 'returns false when share_notes is disabled' do
trip.update!(sharing_settings: { 'share_notes' => false })
expect(trip.share_notes?).to be false
end
end
describe '#share_photos?' do
it 'returns false by default' do
expect(trip.share_photos?).to be false
end
it 'returns true when share_photos is enabled' do
trip.update!(sharing_settings: { 'share_photos' => true })
expect(trip.share_photos?).to be true
end
it 'returns false when share_photos is disabled' do
trip.update!(sharing_settings: { 'share_photos' => false })
expect(trip.share_photos?).to be false
end
end
end

View file

@ -263,222 +263,8 @@ RSpec.describe Stat, type: :model do
end
end
describe 'sharing settings' do
let(:user) { create(:user) }
let(:stat) { create(:stat, year: 2024, month: 6, user: user) }
describe '#sharing_enabled?' do
context 'when sharing_settings is nil' do
before { stat.update_column(:sharing_settings, nil) }
it 'returns false' do
expect(stat.sharing_enabled?).to be false
end
end
context 'when sharing_settings is empty hash' do
before { stat.update(sharing_settings: {}) }
it 'returns false' do
expect(stat.sharing_enabled?).to be false
end
end
context 'when enabled is false' do
before { stat.update(sharing_settings: { 'enabled' => false }) }
it 'returns false' do
expect(stat.sharing_enabled?).to be false
end
end
context 'when enabled is true' do
before { stat.update(sharing_settings: { 'enabled' => true }) }
it 'returns true' do
expect(stat.sharing_enabled?).to be true
end
end
context 'when enabled is a string "true"' do
before { stat.update(sharing_settings: { 'enabled' => 'true' }) }
it 'returns false (strict boolean check)' do
expect(stat.sharing_enabled?).to be false
end
end
end
describe '#sharing_expired?' do
context 'when sharing_settings is nil' do
before { stat.update_column(:sharing_settings, nil) }
it 'returns false' do
expect(stat.sharing_expired?).to be false
end
end
context 'when expiration is blank' do
before { stat.update(sharing_settings: { 'enabled' => true }) }
it 'returns false' do
expect(stat.sharing_expired?).to be false
end
end
context 'when expiration is present but expires_at is blank' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h'
})
end
it 'returns true' do
expect(stat.sharing_expired?).to be true
end
end
context 'when expires_at is in the future' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.from_now.iso8601
})
end
it 'returns false' do
expect(stat.sharing_expired?).to be false
end
end
context 'when expires_at is in the past' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
end
it 'returns true' do
expect(stat.sharing_expired?).to be true
end
end
context 'when expires_at is 1 second in the future' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.second.from_now.iso8601
})
end
it 'returns false (not yet expired)' do
expect(stat.sharing_expired?).to be false
end
end
context 'when expires_at is invalid date string' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 'invalid-date'
})
end
it 'returns true (treats as expired)' do
expect(stat.sharing_expired?).to be true
end
end
context 'when expires_at is nil' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => nil
})
end
it 'returns true' do
expect(stat.sharing_expired?).to be true
end
end
context 'when expires_at is empty string' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => ''
})
end
it 'returns true' do
expect(stat.sharing_expired?).to be true
end
end
end
describe '#public_accessible?' do
context 'when sharing_settings is nil' do
before { stat.update_column(:sharing_settings, nil) }
it 'returns false' do
expect(stat.public_accessible?).to be false
end
end
context 'when sharing is not enabled' do
before { stat.update(sharing_settings: { 'enabled' => false }) }
it 'returns false' do
expect(stat.public_accessible?).to be false
end
end
context 'when sharing is enabled but expired' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
end
it 'returns false' do
expect(stat.public_accessible?).to be false
end
end
context 'when sharing is enabled and not expired' do
before do
stat.update(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.from_now.iso8601
})
end
it 'returns true' do
expect(stat.public_accessible?).to be true
end
end
context 'when sharing is enabled with no expiration' do
before do
stat.update(sharing_settings: { 'enabled' => true })
end
it 'returns true' do
expect(stat.public_accessible?).to be true
end
end
end
describe 'Shareable concern' do
it_behaves_like 'shareable'
end
end
end

View file

@ -156,36 +156,12 @@ RSpec.describe Trip, type: :model do
end
describe 'Shareable concern' do
it_behaves_like 'shareable'
describe 'Trip-specific sharing' do
let(:user) { create(:user) }
let(:trip) { create(:trip, user: user) }
describe 'sharing_uuid generation' do
it 'generates a sharing_uuid on create' do
new_trip = build(:trip, user: user)
expect(new_trip.sharing_uuid).to be_nil
new_trip.save!
expect(new_trip.sharing_uuid).to be_present
end
end
describe '#public_accessible?' do
it 'returns false by default' do
expect(trip.public_accessible?).to be false
end
it 'returns true when sharing is enabled and not expired' do
trip.enable_sharing!(expiration: '24h')
expect(trip.public_accessible?).to be true
end
it 'returns false when sharing is disabled' do
trip.enable_sharing!(expiration: '24h')
trip.disable_sharing!
expect(trip.public_accessible?).to be false
end
end
describe '#enable_sharing!' do
it 'enables sharing with notes and photos options' do
trip.enable_sharing!(expiration: '24h', share_notes: true, share_photos: true)
expect(trip.sharing_enabled?).to be true

View file

@ -35,7 +35,7 @@ RSpec.describe 'Shared::Trips', type: :request do
expect(response.body).to include('Test Trip')
expect(response.body).to include('Trip Route')
expect(response.body).to include('data-controller="public-trip-map"')
expect(response.body).to include('data-controller="shared--trip-map"')
expect(response.body).to include(trip.sharing_uuid)
end
@ -117,17 +117,12 @@ RSpec.describe 'Shared::Trips', type: :request do
before { sign_in user }
context 'enabling sharing' do
it 'enables sharing and returns success' do
it 'enables sharing and redirects to trip' do
patch trip_path(trip),
params: { sharing: { enabled: '1', expiration: '24h' } },
as: :json
params: { sharing: { enabled: '1', expiration: '24h' } }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['success']).to be(true)
expect(json_response['sharing_url']).to be_present
expect(json_response['message']).to eq('Sharing enabled successfully')
expect(response).to redirect_to(trip_path(trip))
expect(flash[:notice]).to eq('Trip was successfully updated.')
trip.reload
expect(trip.sharing_enabled?).to be(true)
@ -136,10 +131,9 @@ RSpec.describe 'Shared::Trips', type: :request do
it 'enables sharing with notes option' do
patch trip_path(trip),
params: { sharing: { enabled: '1', expiration: '24h', share_notes: '1' } },
as: :json
params: { sharing: { enabled: '1', expiration: '24h', share_notes: '1' } }
expect(response).to have_http_status(:success)
expect(response).to redirect_to(trip_path(trip))
trip.reload
expect(trip.sharing_enabled?).to be(true)
@ -148,10 +142,9 @@ RSpec.describe 'Shared::Trips', type: :request do
it 'enables sharing with photos option' do
patch trip_path(trip),
params: { sharing: { enabled: '1', expiration: '24h', share_photos: '1' } },
as: :json
params: { sharing: { enabled: '1', expiration: '24h', share_photos: '1' } }
expect(response).to have_http_status(:success)
expect(response).to redirect_to(trip_path(trip))
trip.reload
expect(trip.sharing_enabled?).to be(true)
@ -160,10 +153,9 @@ RSpec.describe 'Shared::Trips', type: :request do
it 'sets custom expiration when provided' do
patch trip_path(trip),
params: { sharing: { enabled: '1', expiration: '1h' } },
as: :json
params: { sharing: { enabled: '1', expiration: '1h' } }
expect(response).to have_http_status(:success)
expect(response).to redirect_to(trip_path(trip))
trip.reload
expect(trip.sharing_enabled?).to be(true)
expect(trip.sharing_settings['expiration']).to eq('1h')
@ -171,10 +163,9 @@ RSpec.describe 'Shared::Trips', type: :request do
it 'enables permanent sharing' do
patch trip_path(trip),
params: { sharing: { enabled: '1', expiration: 'permanent' } },
as: :json
params: { sharing: { enabled: '1', expiration: 'permanent' } }
expect(response).to have_http_status(:success)
expect(response).to redirect_to(trip_path(trip))
trip.reload
expect(trip.sharing_settings['expiration']).to eq('permanent')
expect(trip.sharing_settings['expires_at']).to be_nil
@ -186,16 +177,12 @@ RSpec.describe 'Shared::Trips', type: :request do
trip.enable_sharing!(expiration: '24h')
end
it 'disables sharing and returns success' do
it 'disables sharing and redirects to trip' do
patch trip_path(trip),
params: { sharing: { enabled: '0' } },
as: :json
params: { sharing: { enabled: '0' } }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['success']).to be(true)
expect(json_response['message']).to eq('Sharing disabled successfully')
expect(response).to redirect_to(trip_path(trip))
expect(flash[:notice]).to eq('Trip was successfully updated.')
trip.reload
expect(trip.sharing_enabled?).to be(false)
@ -205,8 +192,7 @@ RSpec.describe 'Shared::Trips', type: :request do
context 'when trip does not exist' do
it 'returns not found' do
patch trip_path(id: 999999),
params: { sharing: { enabled: '1' } },
as: :json
params: { sharing: { enabled: '1' } }
expect(response).to have_http_status(:not_found)
end
@ -218,8 +204,7 @@ RSpec.describe 'Shared::Trips', type: :request do
it 'returns not found' do
patch trip_path(other_trip),
params: { sharing: { enabled: '1' } },
as: :json
params: { sharing: { enabled: '1' } }
expect(response).to have_http_status(:not_found)
end
@ -229,8 +214,7 @@ RSpec.describe 'Shared::Trips', type: :request do
context 'when user is not signed in' do
it 'returns unauthorized' do
patch trip_path(trip),
params: { sharing: { enabled: '1' } },
as: :json
params: { sharing: { enabled: '1' } }
expect(response).to have_http_status(:unauthorized)
end

View file

@ -0,0 +1,199 @@
# frozen_string_literal: true
RSpec.shared_examples 'shareable' do
let(:user) { create(:user) }
let(:shareable_model) { described_class.name.underscore.to_sym }
let(:shareable) { create(shareable_model, user: user) }
describe '#generate_sharing_uuid' do
it 'generates a UUID before create' do
new_record = build(shareable_model, user: user)
expect(new_record.sharing_uuid).to be_nil
new_record.save!
expect(new_record.sharing_uuid).to be_present
expect(new_record.sharing_uuid).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
end
end
describe '#sharing_enabled?' do
it 'returns false by default' do
expect(shareable.sharing_enabled?).to be false
end
it 'returns true when enabled' do
shareable.update!(sharing_settings: { 'enabled' => true })
expect(shareable.sharing_enabled?).to be true
end
it 'returns false when disabled' do
shareable.update!(sharing_settings: { 'enabled' => false })
expect(shareable.sharing_enabled?).to be false
end
end
describe '#sharing_expired?' do
it 'returns false when no expiration is set' do
expect(shareable.sharing_expired?).to be false
end
it 'returns false when expires_at is in the future' do
shareable.update!(sharing_settings: {
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(shareable.sharing_expired?).to be false
end
it 'returns true when expires_at is in the past' do
shareable.update!(sharing_settings: {
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
expect(shareable.sharing_expired?).to be true
end
it 'returns true when expiration is set but expires_at is missing' do
shareable.update!(sharing_settings: {
'expiration' => '24h',
'expires_at' => nil
})
expect(shareable.sharing_expired?).to be true
end
end
describe '#public_accessible?' do
it 'returns false by default' do
expect(shareable.public_accessible?).to be false
end
it 'returns true when enabled and not expired' do
shareable.update!(sharing_settings: {
'enabled' => true,
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(shareable.public_accessible?).to be true
end
it 'returns false when enabled but expired' do
shareable.update!(sharing_settings: {
'enabled' => true,
'expiration' => '1h',
'expires_at' => 1.hour.ago.iso8601
})
expect(shareable.public_accessible?).to be false
end
it 'returns false when disabled' do
shareable.update!(sharing_settings: {
'enabled' => false,
'expiration' => '24h',
'expires_at' => 1.hour.from_now.iso8601
})
expect(shareable.public_accessible?).to be false
end
end
describe '#enable_sharing!' do
it 'enables sharing with default 24h expiration' do
shareable.enable_sharing!
expect(shareable.sharing_enabled?).to be true
expect(shareable.sharing_settings['expiration']).to eq('24h')
expect(shareable.sharing_settings['expires_at']).to be_present
end
it 'enables sharing with custom expiration' do
shareable.enable_sharing!(expiration: '1h')
expect(shareable.sharing_enabled?).to be true
expect(shareable.sharing_settings['expiration']).to eq('1h')
end
it 'enables sharing with permanent expiration' do
shareable.enable_sharing!(expiration: 'permanent')
expect(shareable.sharing_enabled?).to be true
expect(shareable.sharing_settings['expiration']).to eq('permanent')
expect(shareable.sharing_settings['expires_at']).to be_nil
end
it 'defaults to 24h for invalid expiration' do
shareable.enable_sharing!(expiration: 'invalid')
expect(shareable.sharing_settings['expiration']).to eq('24h')
end
it 'generates a sharing_uuid if not present' do
shareable.update_column(:sharing_uuid, nil)
shareable.enable_sharing!
expect(shareable.sharing_uuid).to be_present
end
it 'keeps existing sharing_uuid' do
original_uuid = shareable.sharing_uuid
shareable.enable_sharing!
expect(shareable.sharing_uuid).to eq(original_uuid)
end
end
describe '#disable_sharing!' do
before do
shareable.enable_sharing!(expiration: '24h')
end
it 'disables sharing' do
shareable.disable_sharing!
expect(shareable.sharing_enabled?).to be false
end
it 'clears expiration settings' do
shareable.disable_sharing!
expect(shareable.sharing_settings['expiration']).to be_nil
expect(shareable.sharing_settings['expires_at']).to be_nil
end
it 'keeps the sharing_uuid' do
original_uuid = shareable.sharing_uuid
shareable.disable_sharing!
expect(shareable.sharing_uuid).to eq(original_uuid)
end
end
describe '#generate_new_sharing_uuid!' do
it 'generates a new UUID' do
original_uuid = shareable.sharing_uuid
shareable.generate_new_sharing_uuid!
expect(shareable.sharing_uuid).not_to eq(original_uuid)
expect(shareable.sharing_uuid).to be_present
end
end
describe '#share_notes?' do
it 'returns false by default' do
expect(shareable.share_notes?).to be false
end
it 'returns true when share_notes is enabled' do
shareable.update!(sharing_settings: { 'share_notes' => true })
expect(shareable.share_notes?).to be true
end
it 'returns false when share_notes is disabled' do
shareable.update!(sharing_settings: { 'share_notes' => false })
expect(shareable.share_notes?).to be false
end
end
describe '#share_photos?' do
it 'returns false by default' do
expect(shareable.share_photos?).to be false
end
it 'returns true when share_photos is enabled' do
shareable.update!(sharing_settings: { 'share_photos' => true })
expect(shareable.share_photos?).to be true
end
it 'returns false when share_photos is disabled' do
shareable.update!(sharing_settings: { 'share_photos' => false })
expect(shareable.share_photos?).to be false
end
end
end