# frozen_string_literal: true require 'rails_helper' RSpec.describe Photoprism::RequestPhotos do let(:user) do create( :user, settings: { 'photoprism_url' => 'http://photoprism.local', 'photoprism_api_key' => 'test_api_key' } ) end let(:start_date) { '2024-01-01' } let(:end_date) { '2024-12-31' } let(:service) { described_class.new(user, start_date: start_date, end_date: end_date) } let(:mock_photo_response) do [ { 'ID' => '82', 'UID' => 'psnveqq089xhy1c3', 'Type' => 'image', 'TypeSrc' => '', 'TakenAt' => '2024-08-18T14:11:05Z', 'TakenAtLocal' => '2024-08-18T16:11:05Z', 'TakenSrc' => 'meta', 'TimeZone' => 'Europe/Prague', 'Path' => '2024/08', 'Name' => '20240818_141105_44E61AED', 'OriginalName' => 'PXL_20240818_141105789', 'Title' => 'Moment / Karlovy Vary / 2024', 'Description' => '', 'Year' => 2024, 'Month' => 8, 'Day' => 18, 'Country' => 'cz', 'Stack' => 0, 'Favorite' => false, 'Private' => false, 'Iso' => 37, 'FocalLength' => 21, 'FNumber' => 2.2, 'Exposure' => '1/347', 'Quality' => 4, 'Resolution' => 10, 'Color' => 2, 'Scan' => false, 'Panorama' => false, 'CameraID' => 8, 'CameraSrc' => 'meta', 'CameraMake' => 'Google', 'CameraModel' => 'Pixel 7 Pro', 'LensID' => 11, 'LensMake' => 'Google', 'LensModel' => 'Pixel 7 Pro front camera 2.74mm f/2.2', 'Altitude' => 423, 'Lat' => 50.11, 'Lng' => 12.12, 'CellID' => 's2:47a09944f33c', 'PlaceID' => 'cz:ciNqTjWuq6NN', 'PlaceSrc' => 'meta', 'PlaceLabel' => 'Karlovy Vary, Severozápad, Czech Republic', 'PlaceCity' => 'Karlovy Vary', 'PlaceState' => 'Severozápad', 'PlaceCountry' => 'cz', 'InstanceID' => '', 'FileUID' => 'fsnveqqeusn692qo', 'FileRoot' => '/', 'FileName' => '2024/08/20240818_141105_44E61AED.jpg', 'Hash' => 'cc5d0f544e52b288d7c8460d2e1bb17fa66e6089', 'Width' => 2736, 'Height' => 3648, 'Portrait' => true, 'Merged' => false, 'CreatedAt' => '2024-12-02T14:25:38Z', 'UpdatedAt' => '2024-12-02T14:25:38Z', 'EditedAt' => '0001-01-01T00:00:00Z', 'CheckedAt' => '2024-12-02T14:36:45Z', 'Files' => nil }, { 'ID' => '81', 'UID' => 'psnveqpl96gcfdzf', 'Type' => 'image', 'TypeSrc' => '', 'TakenAt' => '2024-08-18T14:11:04Z', 'TakenAtLocal' => '2024-08-18T16:11:04Z', 'TakenSrc' => 'meta', 'TimeZone' => 'Europe/Prague', 'Path' => '2024/08', 'Name' => '20240818_141104_E9949CD4', 'OriginalName' => 'PXL_20240818_141104633', 'Title' => 'Portrait / Karlovy Vary / 2024', 'Description' => '', 'Year' => 2024, 'Month' => 8, 'Day' => 18, 'Country' => 'cz', 'Stack' => 0, 'Favorite' => false, 'Private' => false, 'Iso' => 43, 'FocalLength' => 21, 'FNumber' => 2.2, 'Exposure' => '1/356', 'Faces' => 1, 'Quality' => 4, 'Resolution' => 10, 'Color' => 2, 'Scan' => false, 'Panorama' => false, 'CameraID' => 8, 'CameraSrc' => 'meta', 'CameraMake' => 'Google', 'CameraModel' => 'Pixel 7 Pro', 'LensID' => 11, 'LensMake' => 'Google', 'LensModel' => 'Pixel 7 Pro front camera 2.74mm f/2.2', 'Altitude' => 423, 'Lat' => 50.21, 'Lng' => 12.85, 'CellID' => 's2:47a09944f33c', 'PlaceID' => 'cz:ciNqTjWuq6NN', 'PlaceSrc' => 'meta', 'PlaceLabel' => 'Karlovy Vary, Severozápad, Czech Republic', 'PlaceCity' => 'Karlovy Vary', 'PlaceState' => 'Severozápad', 'PlaceCountry' => 'cz', 'InstanceID' => '', 'FileUID' => 'fsnveqp9xsl7onsv', 'FileRoot' => '/', 'FileName' => '2024/08/20240818_141104_E9949CD4.jpg', 'Hash' => 'd5dfadc56a0b63051dfe0b5dec55ff1d81f033b7', 'Width' => 2736, 'Height' => 3648, 'Portrait' => true, 'Merged' => false, 'CreatedAt' => '2024-12-02T14:25:37Z', 'UpdatedAt' => '2024-12-02T14:25:37Z', 'EditedAt' => '0001-01-01T00:00:00Z', 'CheckedAt' => '2024-12-02T14:36:45Z', 'Files' => nil } ] end describe '#call' do context 'with valid credentials' do before do stub_request( :any, "#{user.settings['photoprism_url']}/api/v1/photos?after=#{start_date}&before=#{end_date}&count=1000&public=true&q=&quality=3" ).with( headers: { 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer test_api_key', 'User-Agent' => 'Ruby' } ).to_return( status: 200, body: mock_photo_response.to_json, headers: { 'Content-Type' => 'application/json' } ) stub_request( :any, "#{user.settings['photoprism_url']}/api/v1/photos?after=#{start_date}&before=#{end_date}&count=1000&public=true&q=&quality=3&offset=1000" ).to_return(status: 200, body: [].to_json) end it 'returns photos within the specified date range' do result = service.call expect(result).to be_an(Array) expect(result.first['Title']).to eq('Moment / Karlovy Vary / 2024') end end context 'with missing credentials' do let(:user) { create(:user, settings: {}) } it 'raises error when Photoprism URL is missing' do expect { service.call }.to raise_error(ArgumentError, 'Photoprism URL is missing') end it 'raises error when API key is missing' do user.update(settings: { 'photoprism_url' => 'http://photoprism.local' }) expect { service.call }.to raise_error(ArgumentError, 'Photoprism API key is missing') end end context 'when API returns an error' do before do stub_request( :get, "#{user.settings['photoprism_url']}/api/v1/photos?after=#{start_date}&before=#{end_date}&count=1000&public=true&q=&quality=3" ).to_return(status: 400, body: { status: 400, error: 'Unable to do that' }.to_json) end it 'logs the error' do expect(Rails.logger).to \ receive(:error).with('Photoprism API returned 400: {"status":400,"error":"Unable to do that"}') expect(Rails.logger).to \ receive(:debug).with( "Photoprism API request params: #{{ q: '', public: true, quality: 3, after: start_date, count: 1000, before: end_date }}" ) service.call end end context 'with pagination' do let(:first_page) { [{ 'TakenAtLocal' => "#{start_date}T14:30:00Z" }] } let(:second_page) { [{ 'TakenAtLocal' => "#{start_date}T14:30:00Z" }] } let(:empty_page) { [] } let(:common_headers) do { 'Accept' => 'application/json', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer test_api_key', 'User-Agent' => 'Ruby' } end before do # First page stub_request(:any, "#{user.settings['photoprism_url']}/api/v1/photos") .with( headers: common_headers, query: { after: start_date, before: end_date, count: '1000', public: 'true', q: '', quality: '3' } ) .to_return(status: 200, body: first_page.to_json) # Second page stub_request(:any, "#{user.settings['photoprism_url']}/api/v1/photos") .with( headers: common_headers, query: { after: start_date, before: end_date, count: '1000', public: 'true', q: '', quality: '3', offset: '1000' } ) .to_return(status: 200, body: second_page.to_json) # Last page (empty) stub_request(:any, "#{user.settings['photoprism_url']}/api/v1/photos") .with( headers: common_headers, query: { after: start_date, before: end_date, count: '1000', public: 'true', q: '', quality: '3', offset: '2000' } ) .to_return(status: 200, body: empty_page.to_json) end it 'fetches all pages until empty result' do result = service.call expect(result.size).to eq(2) end end end end