|
23 | 23 | lonlat: 'POINT(-0.1278 51.5074)',
|
24 | 24 | timestamp: timestamp,
|
25 | 25 | user_id: user.id,
|
26 |
| - created_at: anything, |
27 |
| - updated_at: anything |
| 26 | + created_at: Time.current, |
| 27 | + updated_at: Time.current |
28 | 28 | },
|
29 | 29 | {
|
30 | 30 | lonlat: 'POINT(-74.006 40.7128)',
|
31 | 31 | timestamp: timestamp + 1.hour,
|
32 | 32 | user_id: user.id,
|
33 |
| - created_at: anything, |
34 |
| - updated_at: anything |
| 33 | + created_at: Time.current, |
| 34 | + updated_at: Time.current |
35 | 35 | }
|
36 | 36 | ]
|
37 | 37 | end
|
|
43 | 43 | ]
|
44 | 44 | end
|
45 | 45 |
|
46 |
| - it 'processes the points and upserts them to the database' do |
47 |
| - expect(Points::Params).to receive(:new).with(point_params, user.id).and_return(params_service) |
48 |
| - expect(params_service).to receive(:call).and_return(processed_data) |
49 |
| - expect(Point).to receive(:upsert_all) |
50 |
| - .with( |
51 |
| - processed_data, |
52 |
| - unique_by: %i[lonlat timestamp user_id], |
53 |
| - returning: Arel.sql('id, timestamp, ST_X(lonlat::geometry) AS longitude, ST_Y(lonlat::geometry) AS latitude') |
54 |
| - ) |
55 |
| - .and_return(upsert_result) |
| 46 | + describe 'basic point creation' do |
| 47 | + before do |
| 48 | + allow(Points::Params).to receive(:new).with(point_params, user.id).and_return(params_service) |
| 49 | + allow(params_service).to receive(:call).and_return(processed_data) |
| 50 | + end |
| 51 | + |
| 52 | + it 'initializes the params service with correct arguments' do |
| 53 | + expect(Points::Params).to receive(:new).with(point_params, user.id) |
| 54 | + described_class.new(user, point_params).call |
| 55 | + end |
| 56 | + |
| 57 | + it 'calls the params service' do |
| 58 | + expect(params_service).to receive(:call) |
| 59 | + described_class.new(user, point_params).call |
| 60 | + end |
56 | 61 |
|
57 |
| - result = described_class.new(user, point_params).call |
| 62 | + it 'upserts the processed data' do |
| 63 | + expect(Point).to receive(:upsert_all) |
| 64 | + .with( |
| 65 | + processed_data, |
| 66 | + unique_by: %i[lonlat timestamp user_id], |
| 67 | + returning: Arel.sql( |
| 68 | + 'id, timestamp, ST_X(lonlat::geometry) AS longitude, ST_Y(lonlat::geometry) AS latitude' |
| 69 | + ) |
| 70 | + ) |
| 71 | + .and_return(upsert_result) |
58 | 72 |
|
59 |
| - expect(result).to eq(upsert_result) |
| 73 | + described_class.new(user, point_params).call |
| 74 | + end |
| 75 | + |
| 76 | + it 'returns the upsert result' do |
| 77 | + allow(Point).to receive(:upsert_all).and_return(upsert_result) |
| 78 | + result = described_class.new(user, point_params).call |
| 79 | + expect(result).to eq(upsert_result) |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + context 'with duplicate points' do |
| 84 | + let(:duplicate_point_params) do |
| 85 | + { |
| 86 | + locations: [ |
| 87 | + { lat: 51.5074, lon: -0.1278, timestamp: timestamp.iso8601 }, |
| 88 | + { lat: 51.5074, lon: -0.1278, timestamp: timestamp.iso8601 }, # Duplicate |
| 89 | + { lat: 40.7128, lon: -74.0060, timestamp: (timestamp + 1.hour).iso8601 } |
| 90 | + ] |
| 91 | + } |
| 92 | + end |
| 93 | + |
| 94 | + let(:duplicate_processed_data) do |
| 95 | + current_time = Time.current |
| 96 | + [ |
| 97 | + { |
| 98 | + lonlat: 'POINT(-0.1278 51.5074)', |
| 99 | + timestamp: timestamp, |
| 100 | + user_id: user.id, |
| 101 | + created_at: current_time, |
| 102 | + updated_at: current_time |
| 103 | + }, |
| 104 | + { |
| 105 | + lonlat: 'POINT(-0.1278 51.5074)', # Duplicate |
| 106 | + timestamp: timestamp, |
| 107 | + user_id: user.id, |
| 108 | + created_at: current_time, |
| 109 | + updated_at: current_time |
| 110 | + }, |
| 111 | + { |
| 112 | + lonlat: 'POINT(-74.006 40.7128)', |
| 113 | + timestamp: timestamp + 1.hour, |
| 114 | + user_id: user.id, |
| 115 | + created_at: current_time, |
| 116 | + updated_at: current_time |
| 117 | + } |
| 118 | + ] |
| 119 | + end |
| 120 | + |
| 121 | + let(:deduplicated_upsert_result) do |
| 122 | + [ |
| 123 | + Point.new(id: 1, lonlat: 'POINT(-0.1278 51.5074)', timestamp: timestamp), |
| 124 | + Point.new(id: 2, lonlat: 'POINT(-74.006 40.7128)', timestamp: timestamp + 1.hour) |
| 125 | + ] |
| 126 | + end |
| 127 | + |
| 128 | + before do |
| 129 | + allow_any_instance_of(Points::Params).to receive(:call).and_return(duplicate_processed_data) |
| 130 | + end |
| 131 | + |
| 132 | + describe 'deduplication behavior' do |
| 133 | + it 'reduces the number of points to unique combinations' do |
| 134 | + expect(Point).to receive(:upsert_all) do |data, _options| |
| 135 | + expect(data.size).to eq(2) |
| 136 | + deduplicated_upsert_result |
| 137 | + end |
| 138 | + |
| 139 | + described_class.new(user, duplicate_point_params).call |
| 140 | + end |
| 141 | + |
| 142 | + it 'preserves the correct lonlat values' do |
| 143 | + expect(Point).to receive(:upsert_all) do |data, _options| |
| 144 | + expect(data.map { |d| d[:lonlat] }).to match_array(['POINT(-0.1278 51.5074)', 'POINT(-74.006 40.7128)']) |
| 145 | + deduplicated_upsert_result |
| 146 | + end |
| 147 | + |
| 148 | + described_class.new(user, duplicate_point_params).call |
| 149 | + end |
| 150 | + |
| 151 | + it 'preserves the correct timestamps' do |
| 152 | + expect(Point).to receive(:upsert_all) do |data, _options| |
| 153 | + expect(data.map { |d| d[:timestamp] }).to match_array([timestamp, timestamp + 1.hour]) |
| 154 | + deduplicated_upsert_result |
| 155 | + end |
| 156 | + |
| 157 | + described_class.new(user, duplicate_point_params).call |
| 158 | + end |
| 159 | + |
| 160 | + it 'maintains the correct user_id for all points' do |
| 161 | + expect(Point).to receive(:upsert_all) do |data, _options| |
| 162 | + expect(data.map { |d| d[:user_id] }).to all(eq(user.id)) |
| 163 | + deduplicated_upsert_result |
| 164 | + end |
| 165 | + |
| 166 | + described_class.new(user, duplicate_point_params).call |
| 167 | + end |
| 168 | + |
| 169 | + it 'uses the correct unique constraint' do |
| 170 | + expect(Point).to receive(:upsert_all) do |_data, options| |
| 171 | + expect(options[:unique_by]).to eq(%i[lonlat timestamp user_id]) |
| 172 | + deduplicated_upsert_result |
| 173 | + end |
| 174 | + |
| 175 | + described_class.new(user, duplicate_point_params).call |
| 176 | + end |
| 177 | + |
| 178 | + it 'uses the correct returning clause' do |
| 179 | + expect(Point).to receive(:upsert_all) do |_data, options| |
| 180 | + expect(options[:returning]).to eq( |
| 181 | + Arel.sql('id, timestamp, ST_X(lonlat::geometry) AS longitude, ST_Y(lonlat::geometry) AS latitude') |
| 182 | + ) |
| 183 | + deduplicated_upsert_result |
| 184 | + end |
| 185 | + |
| 186 | + described_class.new(user, duplicate_point_params).call |
| 187 | + end |
| 188 | + end |
| 189 | + |
| 190 | + describe 'database interaction' do |
| 191 | + it 'creates only unique points' do |
| 192 | + expect do |
| 193 | + described_class.new(user, duplicate_point_params).call |
| 194 | + end.to change(Point, :count).by(2) |
| 195 | + end |
| 196 | + |
| 197 | + it 'creates points with correct coordinates' do |
| 198 | + described_class.new(user, duplicate_point_params).call |
| 199 | + points = Point.order(:timestamp).last(2) |
| 200 | + |
| 201 | + expect(points[0].lonlat.x).to be_within(0.0001).of(-0.1278) |
| 202 | + expect(points[0].lonlat.y).to be_within(0.0001).of(51.5074) |
| 203 | + expect(points[1].lonlat.x).to be_within(0.0001).of(-74.006) |
| 204 | + expect(points[1].lonlat.y).to be_within(0.0001).of(40.7128) |
| 205 | + end |
| 206 | + end |
60 | 207 | end
|
61 | 208 |
|
62 | 209 | context 'with large datasets' do
|
|
0 commit comments