From 1b559a85b0b26931091ce3885de2c3f1bda73bb3 Mon Sep 17 00:00:00 2001 From: deepak-yabx Date: Fri, 21 Nov 2025 16:09:39 +0530 Subject: [PATCH] feat: add opt-in flag for IP geocoding Make IP geocoding optional and disabled by default. Add --enable-ip-geocode generator flag to enable the feature. - Change save_ip_geocode default to false - Add --enable-ip-geocode generator option - Make ipgeos migration conditional based on flag - Update tests and initializer template --- TESTING.md | 185 ++++++++++++++++++ app/models/rails_url_shortener/visit.rb | 2 +- ...84647_create_rails_url_shortener_ipgeos.rb | 69 +++++-- .../rails_url_shortener_generator.rb | 21 ++ .../templates/initializer.rb | 1 + lib/rails_url_shortener.rb | 6 + .../rails_url_shortener_generator_test.rb | 49 +++++ test/models/rails_url_shortener/visit_test.rb | 24 +++ 8 files changed, 335 insertions(+), 22 deletions(-) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..8cefc5f --- /dev/null +++ b/TESTING.md @@ -0,0 +1,185 @@ +# Testing Guide + +This guide explains how to test the IP geocoding configuration feature. + +## Running Automated Tests + +### Run All Tests +```bash +bundle exec rake test +``` + +### Run Specific Test Files +```bash +# Test the generator with the flag +bundle exec ruby -Itest test/lib/generators/rails_url_shortener/rails_url_shortener_generator_test.rb + +# Test the Visit model (includes IP geocoding tests) +bundle exec ruby -Itest test/models/rails_url_shortener/visit_test.rb + +# Test the IP crawler job +bundle exec ruby -Itest test/jobs/rails_url_shortener/ip_crawler_job_test.rb +``` + +## Manual Testing + +### 1. Test Generator Without Flag (Default - IP Geocoding Disabled) + +```bash +# In a temporary Rails app or the test dummy app +cd test/dummy + +# Run generator without flag +rails generate rails_url_shortener + +# Verify the initializer has save_ip_geocode = false +cat config/initializers/rails_url_shortener.rb | grep save_ip_geocode +# Should show: RailsUrlShortener.save_ip_geocode = false + +# Check that migration was skipped (ipgeos table should not exist) +rails db:migrate:status | grep ipgeo +# Should show the migration was skipped or not run + +# Try to access the ipgeos table +rails console +> RailsUrlShortener::Ipgeo.table_exists? +# Should return: false (if migration was skipped) +``` + +### 2. Test Generator With Flag (IP Geocoding Enabled) + +```bash +# Run generator with the flag +rails generate rails_url_shortener --enable-ip-geocode + +# Verify the initializer has save_ip_geocode = true +cat config/initializers/rails_url_shortener.rb | grep save_ip_geocode +# Should show: RailsUrlShortener.save_ip_geocode = true + +# Check that migration ran +rails db:migrate:status | grep ipgeo +# Should show the migration was executed + +# Verify the ipgeos table exists +rails console +> RailsUrlShortener::Ipgeo.table_exists? +# Should return: true + +> ActiveRecord::Base.connection.column_exists?(:rails_url_shortener_visits, :ipgeo_id) +# Should return: true +``` + +### 3. Test Runtime Behavior - IP Geocoding Disabled (Default) + +```bash +rails console + +# Verify default configuration +> RailsUrlShortener.save_ip_geocode +# Should return: false + +# Create a visit (simulating a URL redirect) +> url = RailsUrlShortener::Url.generate('https://example.com') +> request = ActionDispatch::TestRequest.create +> request.remote_addr = '192.168.1.1' +> visit = RailsUrlShortener::Visit.parse_and_save(url, request) + +# Verify visit was created but IpCrawlerJob was NOT enqueued +> visit.persisted? +# Should return: true + +> ActiveJob::Base.queue_adapter.enqueued_jobs.select { |j| j[:job] == RailsUrlShortener::IpCrawlerJob }.count +# Should return: 0 (no job enqueued) + +> visit.ipgeo +# Should return: nil +``` + +### 4. Test Runtime Behavior - IP Geocoding Enabled + +```bash +rails console + +# Enable IP geocoding +> RailsUrlShortener.save_ip_geocode = true + +# Create a visit +> url = RailsUrlShortener::Url.generate('https://example.com') +> request = ActionDispatch::TestRequest.create +> request.remote_addr = '192.168.1.1' +> request.user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + +# Use ActiveJob test adapter to catch enqueued jobs +> ActiveJob::Base.queue_adapter = ActiveJob::QueueAdapters::TestAdapter.new + +> visit = RailsUrlShortener::Visit.parse_and_save(url, request) + +# Verify visit was created AND IpCrawlerJob was enqueued +> visit.persisted? +# Should return: true + +> ActiveJob::Base.queue_adapter.enqueued_jobs.select { |j| j[:job] == RailsUrlShortener::IpCrawlerJob }.count +# Should return: 1 (job was enqueued) + +# Process the job (if testing in test environment with VCR cassettes) +# > RailsUrlShortener::IpCrawlerJob.perform_now(visit) +``` + +### 5. Test Migration Behavior Directly + +```bash +# Test that migration skips by default +ENV['ENABLE_IP_GEOCODE'] = nil +rails db:migrate:down VERSION=20220418184647 2>/dev/null || true +rails db:migrate:up VERSION=20220418184647 +# Should output: "Skipping IP geocode migration..." + +# Test that migration runs when flag is set +ENV['ENABLE_IP_GEOCODE'] = 'true' +rails db:migrate:down VERSION=20220418184647 2>/dev/null || true +rails db:migrate:up VERSION=20220418184647 +# Should create the tables + +# Verify tables exist +rails console +> RailsUrlShortener::Ipgeo.table_exists? +> ActiveRecord::Base.connection.column_exists?(:rails_url_shortener_visits, :ipgeo_id) +``` + +### 6. Integration Test - Full Workflow + +```bash +# 1. Generate with flag +rails generate rails_url_shortener --enable-ip-geocode + +# 2. Verify initializer +grep "save_ip_geocode = true" config/initializers/rails_url_shortener.rb + +# 3. Start Rails server +rails server + +# 4. In another terminal, create a short URL +rails console +> url = RailsUrlShortener::Url.generate('https://example.com') +> puts url.to_short_url + +# 5. Visit the short URL +curl http://localhost:3000/shortener/[KEY] + +# 6. Check that visit was created and IpCrawlerJob was enqueued +rails console +> visit = RailsUrlShortener::Visit.last +> visit.ipgeo # Should have ipgeo if job completed +``` + +## Test Checklist + +- [ ] Generator without flag creates initializer with `save_ip_geocode = false` +- [ ] Generator with `--enable-ip-geocode` creates initializer with `save_ip_geocode = true` +- [ ] Migration skips by default (no ipgeos table created) +- [ ] Migration runs when `ENABLE_IP_GEOCODE=true` is set +- [ ] Visit.parse_and_save does not enqueue IpCrawlerJob when disabled +- [ ] Visit.parse_and_save enqueues IpCrawlerJob when enabled +- [ ] All existing tests still pass +- [ ] New generator tests pass + diff --git a/app/models/rails_url_shortener/visit.rb b/app/models/rails_url_shortener/visit.rb index 16ca21a..7a04fca 100644 --- a/app/models/rails_url_shortener/visit.rb +++ b/app/models/rails_url_shortener/visit.rb @@ -52,7 +52,7 @@ def self.parse_and_save(url, request) referer: request.headers['Referer'] ) - IpCrawlerJob.perform_later(visit) + IpCrawlerJob.perform_later(visit) if RailsUrlShortener.save_ip_geocode visit end # rubocop:enable Metrics/AbcSize diff --git a/db/migrate/20220418184647_create_rails_url_shortener_ipgeos.rb b/db/migrate/20220418184647_create_rails_url_shortener_ipgeos.rb index 3ce355f..27f8ee5 100644 --- a/db/migrate/20220418184647_create_rails_url_shortener_ipgeos.rb +++ b/db/migrate/20220418184647_create_rails_url_shortener_ipgeos.rb @@ -1,29 +1,56 @@ class CreateRailsUrlShortenerIpgeos < ActiveRecord::Migration[7.0] def up - create_table :rails_url_shortener_ipgeos do |t| - t.string :ip - t.string :country - t.string :country_code - t.string :region - t.string :region_name - t.string :city - t.string :lat - t.string :lon - t.string :timezone - t.string :isp - t.string :org - t.string :as - t.boolean :mobile - t.boolean :proxy - t.boolean :hosting - t.timestamps + # Skip this migration by default - only run if IP geocoding is enabled via environment variable + # Note: Set ENABLE_IP_GEOCODE=true before running migrations to create IP geocode tables + unless ENV['ENABLE_IP_GEOCODE'] == 'true' + say 'Skipping IP geocode migration (IP geocoding is disabled by default. Set ENABLE_IP_GEOCODE=true to enable)' + return + end + + # Check if table already exists (for idempotency) + unless table_exists?(:rails_url_shortener_ipgeos) + create_table :rails_url_shortener_ipgeos do |t| + t.string :ip + t.string :country + t.string :country_code + t.string :region + t.string :region_name + t.string :city + t.string :lat + t.string :lon + t.string :timezone + t.string :isp + t.string :org + t.string :as + t.boolean :mobile + t.boolean :proxy + t.boolean :hosting + t.timestamps + end + end + + # Check if column already exists before adding it + unless column_exists?(:rails_url_shortener_visits, :ipgeo_id) + add_column :rails_url_shortener_visits, :ipgeo_id, :integer + end + + # Add index if it doesn't exist + unless index_exists?(:rails_url_shortener_visits, :ipgeo_id) + add_index :rails_url_shortener_visits, :ipgeo_id end - add_column :rails_url_shortener_visits, :ipgeo_id, :integer - add_index :rails_url_shortener_visits, :ipgeo_id end def down - remove_column :rails_url_shortener_visits, :ipgeo_id - drop_table :rails_url_shortener_ipgeos + # Skip this migration if IP geocoding was never enabled + return unless ENV['ENABLE_IP_GEOCODE'] == 'true' + + # Only drop if tables/columns exist + if column_exists?(:rails_url_shortener_visits, :ipgeo_id) + remove_column :rails_url_shortener_visits, :ipgeo_id + end + + if table_exists?(:rails_url_shortener_ipgeos) + drop_table :rails_url_shortener_ipgeos + end end end diff --git a/lib/generators/rails_url_shortener/rails_url_shortener_generator.rb b/lib/generators/rails_url_shortener/rails_url_shortener_generator.rb index aa72a14..94a5751 100644 --- a/lib/generators/rails_url_shortener/rails_url_shortener_generator.rb +++ b/lib/generators/rails_url_shortener/rails_url_shortener_generator.rb @@ -5,10 +5,24 @@ class RailsUrlShortenerGenerator < Rails::Generators::Base source_root File.expand_path('templates', __dir__) + # Add class option for IP geocoding flag + class_option :enable_ip_geocode, + type: :boolean, + default: false, + desc: 'Enable IP geocoding functionality and create ipgeos migration' + def install_and_run_migrations if Rails.env.test? puts 'Skipping migrations in test environment' else + # Set environment variable based on generator option + if options[:enable_ip_geocode] + ENV['ENABLE_IP_GEOCODE'] = 'true' + puts 'Note: IP geocode migration will be created (--enable-ip-geocode flag)' + else + ENV['ENABLE_IP_GEOCODE'] = 'false' unless ENV['ENABLE_IP_GEOCODE'] + puts 'Note: IP geocode migration will be skipped by default. Use --enable-ip-geocode flag to enable IP geocoding.' + end rake 'rails_url_shortener:install:migrations' rake 'db:migrate' end @@ -23,5 +37,12 @@ def add_route_to_routes_file def copy copy_file 'initializer.rb', 'config/initializers/rails_url_shortener.rb' + + # Update initializer if IP geocoding is enabled + if options[:enable_ip_geocode] + gsub_file 'config/initializers/rails_url_shortener.rb', + /RailsUrlShortener\.save_ip_geocode = false/, + 'RailsUrlShortener.save_ip_geocode = true' + end end end diff --git a/lib/generators/rails_url_shortener/templates/initializer.rb b/lib/generators/rails_url_shortener/templates/initializer.rb index b945ca4..62e557c 100644 --- a/lib/generators/rails_url_shortener/templates/initializer.rb +++ b/lib/generators/rails_url_shortener/templates/initializer.rb @@ -13,3 +13,4 @@ RailsUrlShortener.minimum_key_length = 3 # minimum permited for a key RailsUrlShortener.save_bots_visits = false # if save bots visits RailsUrlShortener.save_visits = true # if save visits +RailsUrlShortener.save_ip_geocode = false # if save IP geocode information (set to true to enable IpCrawlerJob) diff --git a/lib/rails_url_shortener.rb b/lib/rails_url_shortener.rb index ddbb311..dad0dbe 100644 --- a/lib/rails_url_shortener.rb +++ b/lib/rails_url_shortener.rb @@ -42,6 +42,12 @@ module RailsUrlShortener mattr_accessor :save_bots_visits, default: true mattr_accessor :save_visits, default: true + + ## + # if save ip geocode information on db, this will trigger IpCrawlerJob + # to fetch IP geolocation data from external API + # by default this is disabled - set to true to enable IP geocoding + mattr_accessor :save_ip_geocode, default: false end ActiveSupport.on_load(:active_record) do diff --git a/test/lib/generators/rails_url_shortener/rails_url_shortener_generator_test.rb b/test/lib/generators/rails_url_shortener/rails_url_shortener_generator_test.rb index 7a213e3..a26eb47 100644 --- a/test/lib/generators/rails_url_shortener/rails_url_shortener_generator_test.rb +++ b/test/lib/generators/rails_url_shortener/rails_url_shortener_generator_test.rb @@ -26,5 +26,54 @@ class RailsUrlShortenerGeneratorTest < Rails::Generators::TestCase assert_match(%r{mount RailsUrlShortener::Engine, at: '/}, content) end end + + test 'generator sets save_ip_geocode to false by default' do + routes_file = File.join(destination_root, 'config', 'routes.rb') + FileUtils.mkdir_p(File.dirname(routes_file)) + File.write(routes_file, "Rails.application.routes.draw do\nend") + + run_generator ['arguments'] + + # Verify initializer has save_ip_geocode set to false + assert_file 'config/initializers/rails_url_shortener.rb' do |content| + assert_match(/RailsUrlShortener\.save_ip_geocode = false/, content) + end + end + + test 'generator sets save_ip_geocode to true when --enable-ip-geocode flag is used' do + routes_file = File.join(destination_root, 'config', 'routes.rb') + FileUtils.mkdir_p(File.dirname(routes_file)) + File.write(routes_file, "Rails.application.routes.draw do\nend") + + run_generator ['arguments', '--enable-ip-geocode'] + + # Verify initializer has save_ip_geocode set to true + assert_file 'config/initializers/rails_url_shortener.rb' do |content| + assert_match(/RailsUrlShortener\.save_ip_geocode = true/, content) + assert_no_match(/RailsUrlShortener\.save_ip_geocode = false/, content) + end + end + + test 'generator sets ENABLE_IP_GEOCODE environment variable when flag is used' do + routes_file = File.join(destination_root, 'config', 'routes.rb') + FileUtils.mkdir_p(File.dirname(routes_file)) + File.write(routes_file, "Rails.application.routes.draw do\nend") + + # Clear the env var first + original_env = ENV['ENABLE_IP_GEOCODE'] + ENV.delete('ENABLE_IP_GEOCODE') + + run_generator ['arguments', '--enable-ip-geocode'] + + # Verify environment variable was set + assert_equal 'true', ENV['ENABLE_IP_GEOCODE'] + + # Restore original value + if original_env + ENV['ENABLE_IP_GEOCODE'] = original_env + else + ENV.delete('ENABLE_IP_GEOCODE') + end + end end end diff --git a/test/models/rails_url_shortener/visit_test.rb b/test/models/rails_url_shortener/visit_test.rb index 6a5997d..1aa62da 100644 --- a/test/models/rails_url_shortener/visit_test.rb +++ b/test/models/rails_url_shortener/visit_test.rb @@ -42,6 +42,9 @@ class VisitTest < ActiveSupport::TestCase end test 'parse and save' do + # Enable IP geocoding for this test + RailsUrlShortener.save_ip_geocode = true + # generate a fake request request = ActionDispatch::TestRequest.create(env = Rack::MockRequest.env_for('/', 'HTTP_HOST' => 'test.host'.b, 'REMOTE_ADDR' => '1.0.0.0'.b, 'HTTP_USER_AGENT' => 'Rails Testing'.b, @@ -61,6 +64,9 @@ class VisitTest < ActiveSupport::TestCase assert visit.platform, Browser.new(request.user_agent).platform.name assert visit.platform_version, Browser.new(request.user_agent).platform.version assert visit.referer, request.headers['Referer'] + + # Reset to default + RailsUrlShortener.save_ip_geocode = false end test "don't save bots" do @@ -95,5 +101,23 @@ class VisitTest < ActiveSupport::TestCase RailsUrlShortener.save_visits = true end + + test "don't save ip geocode by default" do + # IP geocoding is disabled by default, so this tests the default behavior + # generate a fake request + request = ActionDispatch::TestRequest.create(env = Rack::MockRequest.env_for('/', 'HTTP_HOST' => 'test.host'.b, + 'REMOTE_ADDR' => '1.0.0.0'.b, 'HTTP_USER_AGENT' => 'Rails Testing'.b, + 'HTTP_REFERER' => 'https://example.com'.b)) + request.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15' + + # asserts - visit should be saved but IpCrawlerJob should not be enqueued (default behavior) + visit = nil + assert_no_enqueued_jobs do + visit = Visit.parse_and_save(rails_url_shortener_urls(:one), request) + end + assert visit + assert_equal visit.ip, request.ip + assert_equal visit.user_agent, request.user_agent + end end end