From 86097bb2c279ea401e7e518cd5d19467514f81b8 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 22 Jun 2026 06:17:19 -0400 Subject: [PATCH] Refer to correct scope from `User.matches_ip` and expand `InetContainer` coverage (#39501) --- app/models/user.rb | 2 +- spec/models/ip_block_spec.rb | 1 + spec/models/user_ip_spec.rb | 28 ++++++++++++ .../models/concerns/inet_container.rb | 44 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 spec/models/user_ip_spec.rb create mode 100644 spec/support/examples/models/concerns/inet_container.rb diff --git a/app/models/user.rb b/app/models/user.rb index 282c5bdfb75..b1fbfbd9ff2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,7 +110,7 @@ class User < ApplicationRecord scope :disabled, -> { where(disabled: true) } scope :active, -> { confirmed.signed_in_recently.account_not_suspended } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } - scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) } + scope :matches_ip, ->(value) { left_joins(:ips).merge(UserIp.contained_by(value)).group(users: [:id]) } before_validation :sanitize_role before_create :set_approved diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb index a8b0809511a..595d4cc685c 100644 --- a/spec/models/ip_block_spec.rb +++ b/spec/models/ip_block_spec.rb @@ -4,6 +4,7 @@ require 'rails_helper' RSpec.describe IpBlock do it_behaves_like 'Expireable' + it_behaves_like 'InetContainer' describe 'Validations' do subject { Fabricate.build :ip_block } diff --git a/spec/models/user_ip_spec.rb b/spec/models/user_ip_spec.rb new file mode 100644 index 00000000000..d94817d8a4c --- /dev/null +++ b/spec/models/user_ip_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserIp do + describe 'Scopes' do + describe '.by_latest_used' do + let!(:user) { Fabricate :user, sign_up_ip: '192.168.0.1', created_at: 15.days.ago } + let!(:other_user) { Fabricate :user, sign_up_ip: '10.0.10.0', created_at: 10.days.ago } + + it 'returns records ordered by most recent usage' do + expect(described_class.by_latest_used) + .to eq([other_user.ips.last, user.ips.last]) + end + end + + describe '.contained_by' do + let!(:user) { Fabricate :user, sign_up_ip: '192.168.0.1' } + let!(:other_user) { Fabricate :user, sign_up_ip: '10.0.10.0' } + + it 'returns records ordered by rank' do + expect(described_class.contained_by('192.168.0.0/24')) + .to include(user.ips.last) + .and not_include(other_user.ips.last) + end + end + end +end diff --git a/spec/support/examples/models/concerns/inet_container.rb b/spec/support/examples/models/concerns/inet_container.rb new file mode 100644 index 00000000000..44a3eff6124 --- /dev/null +++ b/spec/support/examples/models/concerns/inet_container.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'InetContainer' do + describe 'Scopes' do + describe '.containing' do + let!(:contained) { Fabricate factory_name, ip: '192.168.0.0/24' } + let!(:uncontained) { Fabricate factory_name, ip: '10.0.0.0/16' } + + it 'returns records containing the value' do + expect(described_class.containing('192.168.0.1')) + .to include(contained) + .and not_include(uncontained) + end + end + + describe '.contained_by' do + let!(:contained) { Fabricate factory_name, ip: '192.168.0.1' } + let!(:uncontained) { Fabricate factory_name, ip: '10.0.10.0' } + + it 'returns records contained by the value' do + expect(described_class.contained_by('192.168.0.0/24')) + .to include(contained) + .and not_include(uncontained) + end + end + + describe '.overlapping_with' do + let!(:contained) { Fabricate factory_name, ip: '192.168.0.0/16' } + let!(:contained_also) { Fabricate factory_name, ip: '192.168.0.1' } + let!(:uncontained) { Fabricate factory_name, ip: '10.0.10.0' } + + it 'returns records containing or contained by the value' do + expect(described_class.overlapping_with('192.168.0.0/24')) + .to include(contained) + .and include(contained_also) + .and not_include(uncontained) + end + end + end + + def factory_name + described_class.name.underscore.to_sym + end +end