mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Add specs for hexagon API and public sharing; remove debug logs
This commit is contained in:
parent
5ff35136f2
commit
34e71b5d17
8 changed files with 465 additions and 27 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
- A cron job to generate daily tracks for users with new points since their last track generation. Being run every 4 hours.
|
- A cron job to generate daily tracks for users with new points since their last track generation. Being run every 4 hours.
|
||||||
- A new month stat page, featuring insights on how user's month went: distance traveled, active days, countries visited and more.
|
- A new month stat page, featuring insights on how user's month went: distance traveled, active days, countries visited and more.
|
||||||
|
- Month stat page can now be shared via public link. User can limit access to the page by sharing period: 1/12/24 hours or permanent.
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@ class Api::V1::Maps::HexagonsController < ApiController
|
||||||
before_action :set_user_and_dates
|
before_action :set_user_and_dates
|
||||||
|
|
||||||
def index
|
def index
|
||||||
Rails.logger.debug "Hexagon API request params: #{params.inspect}"
|
|
||||||
Rails.logger.debug "Hexagon params: #{hexagon_params}"
|
|
||||||
|
|
||||||
service = Maps::HexagonGrid.new(hexagon_params)
|
service = Maps::HexagonGrid.new(hexagon_params)
|
||||||
result = service.call
|
result = service.call
|
||||||
|
|
||||||
|
|
@ -16,13 +13,11 @@ class Api::V1::Maps::HexagonsController < ApiController
|
||||||
render json: result
|
render json: result
|
||||||
rescue Maps::HexagonGrid::BoundingBoxTooLargeError,
|
rescue Maps::HexagonGrid::BoundingBoxTooLargeError,
|
||||||
Maps::HexagonGrid::InvalidCoordinatesError => e
|
Maps::HexagonGrid::InvalidCoordinatesError => e
|
||||||
Rails.logger.error "Hexagon validation error: #{e.message}"
|
|
||||||
render json: { error: e.message }, status: :bad_request
|
render json: { error: e.message }, status: :bad_request
|
||||||
rescue Maps::HexagonGrid::PostGISError => e
|
rescue Maps::HexagonGrid::PostGISError => e
|
||||||
Rails.logger.error "Hexagon PostGIS error: #{e.message}"
|
|
||||||
render json: { error: e.message }, status: :internal_server_error
|
render json: { error: e.message }, status: :internal_server_error
|
||||||
rescue StandardError => e
|
rescue StandardError => _e
|
||||||
handle_service_error(e)
|
handle_service_error
|
||||||
end
|
end
|
||||||
|
|
||||||
def bounds
|
def bounds
|
||||||
|
|
@ -82,21 +77,17 @@ class Api::V1::Maps::HexagonsController < ApiController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_public_sharing_context
|
def set_public_sharing_context
|
||||||
Rails.logger.debug "Public sharing request with UUID: #{params[:uuid]}"
|
|
||||||
@stat = Stat.find_by(sharing_uuid: params[:uuid])
|
@stat = Stat.find_by(sharing_uuid: params[:uuid])
|
||||||
|
|
||||||
unless @stat&.public_accessible?
|
unless @stat&.public_accessible?
|
||||||
Rails.logger.error "Stat not found or not public accessible for UUID: #{params[:uuid]}"
|
render json: {
|
||||||
return render json: {
|
|
||||||
error: 'Shared stats not found or no longer available'
|
error: 'Shared stats not found or no longer available'
|
||||||
}, status: :not_found
|
}, status: :not_found and return
|
||||||
end
|
end
|
||||||
|
|
||||||
@target_user = @stat.user
|
@target_user = @stat.user
|
||||||
@start_date = Date.new(@stat.year, @stat.month, 1).beginning_of_day
|
@start_date = Date.new(@stat.year, @stat.month, 1).beginning_of_day
|
||||||
@end_date = @start_date.end_of_month.end_of_day
|
@end_date = @start_date.end_of_month.end_of_day
|
||||||
|
|
||||||
Rails.logger.debug "Found stat for user #{@target_user.id}, date range: #{@start_date} to #{@end_date}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_authenticated_context
|
def set_authenticated_context
|
||||||
|
|
@ -105,8 +96,7 @@ class Api::V1::Maps::HexagonsController < ApiController
|
||||||
@end_date = params[:end_date]
|
@end_date = params[:end_date]
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_service_error(error)
|
def handle_service_error
|
||||||
Rails.logger.error "Hexagon generation error: #{error.message}\n#{error.backtrace.join("\n")}"
|
|
||||||
render json: { error: 'Failed to generate hexagon grid' }, status: :internal_server_error
|
render json: { error: 'Failed to generate hexagon grid' }, status: :internal_server_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3 class="font-bold text-lg mb-4">🔗 Sharing Settings</h3>
|
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<%= icon 'link' %> Sharing Settings
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div data-controller="sharing-modal"
|
<div data-controller="sharing-modal"
|
||||||
data-sharing-modal-url-value="<%= sharing_stats_path(year: @year, month: @month) %>">
|
data-sharing-modal-url-value="<%= sharing_stats_path(year: @year, month: @month) %>">
|
||||||
|
|
@ -16,7 +18,7 @@
|
||||||
<span class="label-text font-medium">Enable public access</span>
|
<span class="label-text font-medium">Enable public access</span>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
name="enabled"
|
name="enabled"
|
||||||
<%= 'checked' if @stat&.sharing_enabled? %>
|
<%= 'checked' if @stat.sharing_enabled? %>
|
||||||
class="toggle toggle-primary"
|
class="toggle toggle-primary"
|
||||||
data-action="change->sharing-modal#toggleSharing"
|
data-action="change->sharing-modal#toggleSharing"
|
||||||
data-sharing-modal-target="enableToggle" />
|
data-sharing-modal-target="enableToggle" />
|
||||||
|
|
@ -28,7 +30,7 @@
|
||||||
|
|
||||||
<!-- Expiration Settings (shown when enabled) -->
|
<!-- Expiration Settings (shown when enabled) -->
|
||||||
<div data-sharing-modal-target="expirationSettings"
|
<div data-sharing-modal-target="expirationSettings"
|
||||||
class="<%= 'hidden' unless @stat&.sharing_enabled? %>">
|
class="<%= 'hidden' unless @stat.sharing_enabled? %>">
|
||||||
|
|
||||||
<div class="form-control mb-4">
|
<div class="form-control mb-4">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
|
|
@ -57,11 +59,11 @@
|
||||||
readonly
|
readonly
|
||||||
class="input input-bordered join-item flex-1"
|
class="input input-bordered join-item flex-1"
|
||||||
data-sharing-modal-target="sharingLink"
|
data-sharing-modal-target="sharingLink"
|
||||||
value="<%= @stat&.sharing_enabled? ? public_stat_url(@stat.sharing_uuid) : '' %>" />
|
value="<%= @stat.sharing_enabled? ? public_stat_url(@stat.sharing_uuid) : '' %>" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-outline join-item"
|
class="btn btn-outline join-item"
|
||||||
data-action="click->sharing-modal#copyLink">
|
data-action="click->sharing-modal#copyLink">
|
||||||
📋 Copy
|
<%= icon 'copy' %> Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
|
|
@ -73,14 +75,11 @@
|
||||||
|
|
||||||
<!-- Privacy Notice (always visible) -->
|
<!-- Privacy Notice (always visible) -->
|
||||||
<div class="alert alert-info mb-4">
|
<div class="alert alert-info mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
<%= icon 'info' %>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
||||||
</svg>
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold">Privacy Protection</h3>
|
<h3 class="font-bold">Privacy Protection</h3>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
• Exact coordinates are hidden (approximate locations only)<br>
|
• Exact coordinates are hidden<br>
|
||||||
• Map interaction is disabled<br>
|
|
||||||
• Personal information is not included
|
• Personal information is not included
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ FactoryBot.define do
|
||||||
month { 1 }
|
month { 1 }
|
||||||
distance { 1000 } # 1 km
|
distance { 1000 } # 1 km
|
||||||
user
|
user
|
||||||
|
sharing_settings { {} }
|
||||||
|
sharing_uuid { SecureRandom.uuid }
|
||||||
toponyms do
|
toponyms do
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -16,5 +18,31 @@ FactoryBot.define do
|
||||||
}, { 'cities' => [], 'country' => nil }
|
}, { 'cities' => [], 'country' => nil }
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :with_sharing_enabled do
|
||||||
|
after(:create) do |stat, evaluator|
|
||||||
|
stat.enable_sharing!(expiration: 'permanent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :with_sharing_disabled do
|
||||||
|
sharing_settings do
|
||||||
|
{
|
||||||
|
'enabled' => false,
|
||||||
|
'expiration' => nil,
|
||||||
|
'expires_at' => nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :with_sharing_expired do
|
||||||
|
sharing_settings do
|
||||||
|
{
|
||||||
|
'enabled' => true,
|
||||||
|
'expiration' => '1h',
|
||||||
|
'expires_at' => 1.hour.ago.iso8601
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -111,9 +111,9 @@ RSpec.describe Users::MailerSendingJob, type: :job do
|
||||||
it 'raises ActiveRecord::RecordNotFound' do
|
it 'raises ActiveRecord::RecordNotFound' do
|
||||||
user.destroy
|
user.destroy
|
||||||
|
|
||||||
expect {
|
expect do
|
||||||
described_class.perform_now(user.id, 'welcome')
|
described_class.perform_now(user.id, 'welcome')
|
||||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
end.not_to raise_error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -329,5 +329,11 @@ RSpec.describe User, type: :model do
|
||||||
expect { user.export_data }.to have_enqueued_job(Users::ExportDataJob).with(user.id)
|
expect { user.export_data }.to have_enqueued_job(Users::ExportDataJob).with(user.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#timezone' do
|
||||||
|
it 'returns the app timezone' do
|
||||||
|
expect(user.timezone).to eq(Time.zone.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
267
spec/requests/api/v1/maps/hexagons_spec.rb
Normal file
267
spec/requests/api/v1/maps/hexagons_spec.rb
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::V1::Maps::Hexagons', type: :request do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
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 'GET /api/v1/maps/hexagons' do
|
||||||
|
let(:valid_params) do
|
||||||
|
{
|
||||||
|
min_lon: -74.1,
|
||||||
|
min_lat: 40.6,
|
||||||
|
max_lon: -73.9,
|
||||||
|
max_lat: 40.8,
|
||||||
|
hex_size: 1000,
|
||||||
|
start_date: '2024-06-01T00:00:00Z',
|
||||||
|
end_date: '2024-06-30T23:59:59Z'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid API key authentication' do
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{user.api_key}" } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create test points within the date range and bounding box
|
||||||
|
10.times do |i|
|
||||||
|
create(:point,
|
||||||
|
user:,
|
||||||
|
latitude: 40.7 + (i * 0.001), # Slightly different coordinates
|
||||||
|
longitude: -74.0 + (i * 0.001),
|
||||||
|
timestamp: Time.new(2024, 6, 15, 12, i).to_i) # Different times
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns hexagon data successfully' do
|
||||||
|
get '/api/v1/maps/hexagons', params: valid_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to have_key('type')
|
||||||
|
expect(json_response['type']).to eq('FeatureCollection')
|
||||||
|
expect(json_response).to have_key('features')
|
||||||
|
expect(json_response['features']).to be_an(Array)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires all bbox parameters' do
|
||||||
|
incomplete_params = valid_params.except(:min_lon)
|
||||||
|
|
||||||
|
get '/api/v1/maps/hexagons', params: incomplete_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to include('Missing required parameters')
|
||||||
|
expect(json_response['error']).to include('min_lon')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles service validation errors' do
|
||||||
|
invalid_params = valid_params.merge(min_lon: 200) # Invalid longitude
|
||||||
|
|
||||||
|
get '/api/v1/maps/hexagons', params: invalid_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses custom hex_size when provided' do
|
||||||
|
custom_params = valid_params.merge(hex_size: 500)
|
||||||
|
|
||||||
|
get '/api/v1/maps/hexagons', params: custom_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with public sharing UUID' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
|
||||||
|
let(:uuid_params) { valid_params.merge(uuid: stat.sharing_uuid) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create test points within the stat's month
|
||||||
|
15.times do |i|
|
||||||
|
create(:point,
|
||||||
|
user:,
|
||||||
|
latitude: 40.7 + (i * 0.002),
|
||||||
|
longitude: -74.0 + (i * 0.002),
|
||||||
|
timestamp: Time.new(2024, 6, 20, 10, i).to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns hexagon data without API key' do
|
||||||
|
get '/api/v1/maps/hexagons', params: uuid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to have_key('type')
|
||||||
|
expect(json_response['type']).to eq('FeatureCollection')
|
||||||
|
expect(json_response).to have_key('features')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses stat date range automatically' do
|
||||||
|
# Points outside the stat's month should not be included
|
||||||
|
5.times do |i|
|
||||||
|
create(:point,
|
||||||
|
user:,
|
||||||
|
latitude: 40.7 + (i * 0.003),
|
||||||
|
longitude: -74.0 + (i * 0.003),
|
||||||
|
timestamp: Time.new(2024, 7, 1, 8, i).to_i) # July points
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/api/v1/maps/hexagons', params: uuid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid sharing UUID' do
|
||||||
|
it 'returns not found' do
|
||||||
|
invalid_uuid_params = valid_params.merge(uuid: 'invalid-uuid')
|
||||||
|
|
||||||
|
get '/api/v1/maps/hexagons', params: invalid_uuid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with expired sharing' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_expired, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
it 'returns not found' do
|
||||||
|
get '/api/v1/maps/hexagons', params: uuid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with disabled sharing' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_disabled, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
it 'returns not found' do
|
||||||
|
get '/api/v1/maps/hexagons', params: uuid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without authentication' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get '/api/v1/maps/hexagons', params: valid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid API key' do
|
||||||
|
let(:headers) { { 'Authorization' => 'Bearer invalid-key' } }
|
||||||
|
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get '/api/v1/maps/hexagons', params: valid_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /api/v1/maps/hexagons/bounds' do
|
||||||
|
context 'with valid API key authentication' do
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{user.api_key}" } }
|
||||||
|
let(:date_params) do
|
||||||
|
{
|
||||||
|
start_date: Time.new(2024, 6, 1).to_i,
|
||||||
|
end_date: Time.new(2024, 6, 30, 23, 59, 59).to_i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create test points within the date range
|
||||||
|
create(:point, user:, latitude: 40.6, longitude: -74.1, timestamp: Time.new(2024, 6, 1, 12, 0).to_i)
|
||||||
|
create(:point, user:, latitude: 40.8, longitude: -73.9, timestamp: Time.new(2024, 6, 30, 15, 0).to_i)
|
||||||
|
create(:point, user:, latitude: 40.7, longitude: -74.0, timestamp: Time.new(2024, 6, 15, 10, 0).to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns bounding box for user data' do
|
||||||
|
get '/api/v1/maps/hexagons/bounds', params: date_params, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to include('min_lat', 'max_lat', 'min_lng', 'max_lng', 'point_count')
|
||||||
|
expect(json_response['min_lat']).to eq(40.6)
|
||||||
|
expect(json_response['max_lat']).to eq(40.8)
|
||||||
|
expect(json_response['min_lng']).to eq(-74.1)
|
||||||
|
expect(json_response['max_lng']).to eq(-73.9)
|
||||||
|
expect(json_response['point_count']).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns not found when no points exist in date range' do
|
||||||
|
get '/api/v1/maps/hexagons/bounds',
|
||||||
|
params: { start_date: '2023-01-01T00:00:00Z', end_date: '2023-01-31T23:59:59Z' },
|
||||||
|
headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('No data found for the specified date range')
|
||||||
|
expect(json_response['point_count']).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with public sharing UUID' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Create test points within the stat's month
|
||||||
|
create(:point, user:, latitude: 41.0, longitude: -74.5, timestamp: Time.new(2024, 6, 5, 9, 0).to_i)
|
||||||
|
create(:point, user:, latitude: 41.2, longitude: -74.2, timestamp: Time.new(2024, 6, 25, 14, 0).to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns bounds for the shared stat period' do
|
||||||
|
get '/api/v1/maps/hexagons/bounds', params: { uuid: stat.sharing_uuid }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to include('min_lat', 'max_lat', 'min_lng', 'max_lng', 'point_count')
|
||||||
|
expect(json_response['min_lat']).to eq(41.0)
|
||||||
|
expect(json_response['max_lat']).to eq(41.2)
|
||||||
|
expect(json_response['point_count']).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid sharing UUID' do
|
||||||
|
it 'returns not found' do
|
||||||
|
get '/api/v1/maps/hexagons/bounds', params: { uuid: 'invalid-uuid' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without authentication' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get '/api/v1/maps/hexagons/bounds',
|
||||||
|
params: { start_date: '2024-06-01T00:00:00Z', end_date: '2024-06-30T23:59:59Z' }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -111,4 +111,151 @@ RSpec.describe '/stats', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'public sharing' do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
describe 'GET /public/:uuid' do
|
||||||
|
context 'with valid sharing UUID' do
|
||||||
|
before do
|
||||||
|
# Create some test points for data bounds calculation
|
||||||
|
create_list(:point, 5, user:, timestamp: Time.new(2024, 6, 15).to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the public month view' do
|
||||||
|
get public_stat_url(stat.sharing_uuid)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Monthly Digest')
|
||||||
|
expect(response.body).to include('June 2024')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes required content in response' do
|
||||||
|
get public_stat_url(stat.sharing_uuid)
|
||||||
|
|
||||||
|
expect(response.body).to include('June 2024')
|
||||||
|
expect(response.body).to include('Monthly Digest')
|
||||||
|
expect(response.body).to include('data-public-stat-map-uuid-value')
|
||||||
|
expect(response.body).to include(stat.sharing_uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid sharing UUID' do
|
||||||
|
it 'redirects to root with alert' do
|
||||||
|
get public_stat_url('invalid-uuid')
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with expired sharing' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_expired, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
it 'redirects to root with alert' do
|
||||||
|
get public_stat_url(stat.sharing_uuid)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with disabled sharing' do
|
||||||
|
let(:stat) { create(:stat, :with_sharing_disabled, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
it 'redirects to root with alert' do
|
||||||
|
get public_stat_url(stat.sharing_uuid)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
expect(flash[:alert]).to eq('Shared stats not found or no longer available')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when stat has no points' do
|
||||||
|
it 'renders successfully' do
|
||||||
|
get public_stat_url(stat.sharing_uuid)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.body).to include('Monthly Digest')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /update_sharing' do
|
||||||
|
context 'when user is signed in' do
|
||||||
|
let!(:stat_to_share) { create(:stat, user:, year: 2024, month: 6) }
|
||||||
|
|
||||||
|
before { sign_in user }
|
||||||
|
|
||||||
|
context 'enabling sharing' do
|
||||||
|
it 'enables sharing and returns success' do
|
||||||
|
patch sharing_stats_path(year: 2024, month: 6),
|
||||||
|
params: { enabled: '1' },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
stat_to_share.reload
|
||||||
|
expect(stat_to_share.sharing_enabled?).to be(true)
|
||||||
|
expect(stat_to_share.sharing_uuid).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets custom expiration when provided' do
|
||||||
|
patch sharing_stats_path(year: 2024, month: 6),
|
||||||
|
params: { enabled: '1', expiration: '1_week' },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
stat_to_share.reload
|
||||||
|
expect(stat_to_share.sharing_enabled?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'disabling sharing' do
|
||||||
|
let!(:enabled_stat) { create(:stat, :with_sharing_enabled, user:, year: 2024, month: 7) }
|
||||||
|
|
||||||
|
it 'disables sharing and returns success' do
|
||||||
|
patch sharing_stats_path(year: 2024, month: 7),
|
||||||
|
params: { enabled: '0' },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
enabled_stat.reload
|
||||||
|
expect(enabled_stat.sharing_enabled?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when stat does not exist' do
|
||||||
|
it 'returns not found' do
|
||||||
|
patch sharing_stats_path(year: 2024, month: 12),
|
||||||
|
params: { enabled: '1' },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is not signed in' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
patch sharing_stats_path(year: 2024, month: 6),
|
||||||
|
params: { enabled: '1' },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue