Skip to content

Instantly share code, notes, and snippets.

@JeremiahChurch
Last active November 11, 2022 00:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JeremiahChurch/5a36880bb6ce55cd5e882413dac716a0 to your computer and use it in GitHub Desktop.
Save JeremiahChurch/5a36880bb6ce55cd5e882413dac716a0 to your computer and use it in GitHub Desktop.
Custom Subdomains from Models in Rails & Rspec testing too
class SubdomainConstraint
# used in routes to generate subdomains
# note there is a cache buster in company.rb in case the subdomain is updated - `reload_routes`
# update it if we write any additional cache entries here
RESERVED_SUB_DOMAINS = %w[
www app admin test staging help support wwww cdn owner media intercom mail r _domainkey blog webhooks webhook
hooks api api-test api-staging referral outbound mail webmail w3 mailboxes io heroku inbound google search
].freeze
def initialize
generate_subdomains
end
def generate_subdomains
dynamic_subdomains = Company.where.not(subdomain: nil).pluck(:subdomain)
@domains = dynamic_subdomains - RESERVED_SUB_DOMAINS
set_timestamp
# done to allow codeship to run assets:precompile in prod mode without touching the DB
rescue ActiveRecord::StatementInvalid
end
def set_timestamp
Rails.cache.write('rails_routes_ts', Digest::MD5.hexdigest(@domains.inspect), expires_in: 1.day)
rescue StandardError
warn "WARNING - Error in set_timestamp method, maybe cache host is down? (method:#{__method__} in #{__FILE__}"
end
def matches?(request)
check_if_expired_routes?
# In development/staging allow multiple subdomains like http://company-614.lvh.me:3000/
subdomain_to_check = !Rails.env.production? ? request.subdomains.first : request.subdomain
@domains.include?(subdomain_to_check)
rescue StandardError
warn "WARNING - Error detecting Route auth for current domain #{request.host} (method:#{__method__} in #{__FILE__}"
false
end
def check_if_expired_routes?
generate_subdomains if Rails.cache.fetch('rails_routes_ts') != Digest::MD5.hexdigest(@domains.inspect)
rescue StandardError
warn "WARNING - Error in check_if_expired_routes? method, maybe cache host is down? (method:#{__method__} in #{__FILE__}"
end
end
class Company < ApplicationRecord
SUBDOMAIN_REGEX = /\A^[a-z\d]+(-[a-z\d]+)*\z/i # I normally put this in application_record.rb or somewhere else but this is easier for a gist
# associations
has_many :users
# validations
validates :subdomain, length: 3..64, allow_nil: true, uniqueness: true,
format: { with: SUBDOMAIN_REGEX, message: 'Valid subdomain characters only' }
private
# related to subdomain_constraint.rb
def reload_routes
return unless saved_change_to_subdomain?
Rails.cache.delete('rails_routes_ts')
end
end
Rails.application.routes.draw do
# keep this above other routes otherwise it will be overridden
namespace :subdomain, constraints: SubdomainConstraint.new, path: nil do
# whatever subdomain routes you want served - note the namespace - you can change it of course
end
# ... generic routes
end
# Add additional requires below this line. Rails is not loaded until this point!
# ...
require 'support/subdomain_support' # After any SSL - but up top by the rest of your requires
# ...
# Support for Rspec / Capybara subdomain integration testing
# https://gist.github.com/turadg/5399790
def switch_to_subdomain(subdomain)
# lvh.me always resolves to 127.0.0.1
hostname = subdomain ? "#{subdomain}.lvh.me" : 'localhost'
Capybara.app_host = "http://#{hostname}:#{Capybara.server_port}"
end
def switch_to_main_domain
switch_to_subdomain nil
end
Capybara.configure do |config|
config.always_include_port = true
end

rspec usage is something like:

before(:each) do
  switch_to_subdomain("#{test_company.subdomain}")
end

after(:each) do
  switch_to_main_domain # keeps everything working fine with random test order
end

and then whatever you want to test on the subdomains

accessing in dev

lvh.me and other services redirect to localhost or 127.0.0.1 for testing - work great for testing subdomains. if you aren't working directly on machine (I work on a VM sometimes.) Then just add the subdomains you work with to your hosts file.

subdomain.lvh.me:3000

You should also add whatever loopback service you use to your CI server's host file so any outages don't cause CI problems

notes:

models and attributes aren't magic, use whatever you like.

files are named with dashes in place of slashes just in case you're super fresh. The first time I picked up subdomains from railscast I was two weeks in to rails and not familiar - hopefully that helps someone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment