|
| 1 | +require 'test_helper' |
| 2 | + |
| 3 | +class CustomDomainTest < ActiveSupport::TestCase |
| 4 | + setup do |
| 5 | + @site = sites(:mysite) |
| 6 | + @custom_domain = CustomDomain.new( |
| 7 | + site: @site, |
| 8 | + domain: 'example.com' |
| 9 | + ) |
| 10 | + end |
| 11 | + |
| 12 | + test 'valid custom domain' do |
| 13 | + assert @custom_domain.valid? |
| 14 | + end |
| 15 | + |
| 16 | + test 'domain is required' do |
| 17 | + @custom_domain.domain = nil |
| 18 | + refute @custom_domain.valid? |
| 19 | + assert_includes @custom_domain.errors[:domain], "can't be blank" |
| 20 | + end |
| 21 | + |
| 22 | + test 'domain must be unique' do |
| 23 | + @custom_domain.save! |
| 24 | + another_site = Site.create!(name: 'another-site', user: @site.user, empty: @site.empty) |
| 25 | + duplicate = CustomDomain.new(site: another_site, domain: 'example.com') |
| 26 | + refute duplicate.valid? |
| 27 | + assert_includes duplicate.errors[:domain], "has already been taken" |
| 28 | + end |
| 29 | + |
| 30 | + test 'domain normalization downcases domain' do |
| 31 | + @custom_domain.domain = 'EXAMPLE.COM' |
| 32 | + @custom_domain.valid? |
| 33 | + assert_equal 'example.com', @custom_domain.domain |
| 34 | + end |
| 35 | + |
| 36 | + test 'domain normalization strips whitespace' do |
| 37 | + @custom_domain.domain = ' example.com ' |
| 38 | + @custom_domain.valid? |
| 39 | + assert_equal 'example.com', @custom_domain.domain |
| 40 | + end |
| 41 | + |
| 42 | + test 'domain format validation' do |
| 43 | + # Valid domains |
| 44 | + [ |
| 45 | + 'example.com', |
| 46 | + 'sub.example.com', |
| 47 | + 'my-site.example.com', |
| 48 | + 'a.b.c.example.com', |
| 49 | + '123.example.com', |
| 50 | + ].each do |valid_domain| |
| 51 | + @custom_domain.domain = valid_domain |
| 52 | + assert @custom_domain.valid?, "#{valid_domain} should be valid" |
| 53 | + end |
| 54 | + |
| 55 | + # Invalid domains |
| 56 | + [ |
| 57 | + 'example', # no TLD |
| 58 | + '-example.com', # starts with dash |
| 59 | + 'example-.com', # ends with dash |
| 60 | + 'exam ple.com', # contains space |
| 61 | + 'example..com', # double dot |
| 62 | + '.example.com', # starts with dot |
| 63 | + 'example.com.', # ends with dot |
| 64 | + ].each do |invalid_domain| |
| 65 | + @custom_domain.domain = invalid_domain |
| 66 | + refute @custom_domain.valid?, "#{invalid_domain} should be invalid" |
| 67 | + end |
| 68 | + end |
| 69 | + |
| 70 | + test 'blocks exact reserved domains' do |
| 71 | + ['tiddlyhost.com', 'tiddlyspot.com', 'localhost'].each do |reserved| |
| 72 | + @custom_domain.domain = reserved |
| 73 | + refute @custom_domain.valid? |
| 74 | + assert_includes @custom_domain.errors[:domain], "cannot use reserved domains or their subdomains" |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + test 'blocks subdomains of reserved domains' do |
| 79 | + [ |
| 80 | + 'www.tiddlyhost.com', |
| 81 | + 'admin.tiddlyhost.com', |
| 82 | + 'api.tiddlyhost.com', |
| 83 | + 'foo.tiddlyspot.com', |
| 84 | + 'bar.baz.tiddlyhost.com', |
| 85 | + ].each do |reserved_subdomain| |
| 86 | + @custom_domain.domain = reserved_subdomain |
| 87 | + refute @custom_domain.valid?, "#{reserved_subdomain} should be blocked" |
| 88 | + assert_includes @custom_domain.errors[:domain], "cannot use reserved domains or their subdomains" |
| 89 | + end |
| 90 | + end |
| 91 | + |
| 92 | + test 'allows domains containing reserved words but not as subdomain' do |
| 93 | + # These should be allowed since they're not subdomains |
| 94 | + [ |
| 95 | + 'tiddlyhostfan.com', |
| 96 | + 'mytiddlyhost.org', |
| 97 | + ].each do |allowed_domain| |
| 98 | + @custom_domain.domain = allowed_domain |
| 99 | + assert @custom_domain.valid?, "#{allowed_domain} should be allowed" |
| 100 | + end |
| 101 | + end |
| 102 | + |
| 103 | + test 'generates verification token on create' do |
| 104 | + assert_nil @custom_domain.verification_token |
| 105 | + @custom_domain.save! |
| 106 | + assert_not_nil @custom_domain.verification_token |
| 107 | + assert_equal 32, @custom_domain.verification_token.length # hex(16) = 32 chars |
| 108 | + end |
| 109 | + |
| 110 | + test 'default status is pending_verification' do |
| 111 | + @custom_domain.save! |
| 112 | + assert @custom_domain.pending_verification? |
| 113 | + assert_equal 0, @custom_domain.status_before_type_cast |
| 114 | + end |
| 115 | + |
| 116 | + test 'default ssl_status is pending' do |
| 117 | + @custom_domain.save! |
| 118 | + assert @custom_domain.ssl_pending? |
| 119 | + assert_equal 0, @custom_domain.ssl_status_before_type_cast |
| 120 | + end |
| 121 | + |
| 122 | + test 'status enum states' do |
| 123 | + @custom_domain.save! |
| 124 | + |
| 125 | + @custom_domain.pending_verification! |
| 126 | + assert @custom_domain.pending_verification? |
| 127 | + |
| 128 | + @custom_domain.verified! |
| 129 | + assert @custom_domain.verified? |
| 130 | + |
| 131 | + @custom_domain.active! |
| 132 | + assert @custom_domain.active? |
| 133 | + |
| 134 | + @custom_domain.failed! |
| 135 | + assert @custom_domain.failed? |
| 136 | + end |
| 137 | + |
| 138 | + test 'ssl_status enum states with prefix' do |
| 139 | + @custom_domain.save! |
| 140 | + |
| 141 | + @custom_domain.ssl_pending! |
| 142 | + assert @custom_domain.ssl_pending? |
| 143 | + |
| 144 | + @custom_domain.ssl_issued! |
| 145 | + assert @custom_domain.ssl_issued? |
| 146 | + |
| 147 | + @custom_domain.ssl_expired! |
| 148 | + assert @custom_domain.ssl_expired? |
| 149 | + |
| 150 | + @custom_domain.ssl_failed! |
| 151 | + assert @custom_domain.ssl_failed? |
| 152 | + end |
| 153 | + |
| 154 | + test 'fully_active? requires both active status and issued SSL' do |
| 155 | + @custom_domain.save! |
| 156 | + |
| 157 | + # Not active yet |
| 158 | + @custom_domain.pending_verification! |
| 159 | + @custom_domain.ssl_pending! |
| 160 | + refute @custom_domain.fully_active? |
| 161 | + |
| 162 | + # Active but no SSL |
| 163 | + @custom_domain.active! |
| 164 | + @custom_domain.ssl_pending! |
| 165 | + refute @custom_domain.fully_active? |
| 166 | + |
| 167 | + # SSL issued but not active |
| 168 | + @custom_domain.verified! |
| 169 | + @custom_domain.ssl_issued! |
| 170 | + refute @custom_domain.fully_active? |
| 171 | + |
| 172 | + # Both active and SSL issued |
| 173 | + @custom_domain.active! |
| 174 | + @custom_domain.ssl_issued! |
| 175 | + assert @custom_domain.fully_active? |
| 176 | + end |
| 177 | + |
| 178 | + test 'fully_active scope' do |
| 179 | + @custom_domain.save! |
| 180 | + |
| 181 | + # Create additional sites for testing (one site = one custom domain) |
| 182 | + pending_site = Site.create!(name: 'pending-site', user: @site.user, empty: @site.empty) |
| 183 | + active_site = Site.create!(name: 'active-site', user: @site.user, empty: @site.empty) |
| 184 | + |
| 185 | + pending_domain = CustomDomain.create!(site: pending_site, domain: 'pending.com') |
| 186 | + active_domain = CustomDomain.create!( |
| 187 | + site: active_site, |
| 188 | + domain: 'active.com', |
| 189 | + status: :active, |
| 190 | + ssl_status: :issued |
| 191 | + ) |
| 192 | + |
| 193 | + fully_active_domains = CustomDomain.fully_active |
| 194 | + assert_includes fully_active_domains, active_domain |
| 195 | + refute_includes fully_active_domains, pending_domain |
| 196 | + refute_includes fully_active_domains, @custom_domain |
| 197 | + end |
| 198 | + |
| 199 | + test 'expiring_soon scope only includes issued certificates' do |
| 200 | + @custom_domain.save! |
| 201 | + |
| 202 | + # Create additional sites for testing (one site = one custom domain) |
| 203 | + expiring_site = Site.create!(name: 'expiring-site', user: @site.user, empty: @site.empty) |
| 204 | + safe_site = Site.create!(name: 'safe-site', user: @site.user, empty: @site.empty) |
| 205 | + pending_site = Site.create!(name: 'pending-cert-site', user: @site.user, empty: @site.empty) |
| 206 | + |
| 207 | + # Create domains with different SSL states |
| 208 | + expired_soon = CustomDomain.create!( |
| 209 | + site: expiring_site, |
| 210 | + domain: 'expires-soon.com', |
| 211 | + ssl_status: :issued, |
| 212 | + ssl_expires_at: 15.days.from_now |
| 213 | + ) |
| 214 | + |
| 215 | + not_expiring = CustomDomain.create!( |
| 216 | + site: safe_site, |
| 217 | + domain: 'not-expiring.com', |
| 218 | + ssl_status: :issued, |
| 219 | + ssl_expires_at: 60.days.from_now |
| 220 | + ) |
| 221 | + |
| 222 | + pending_cert = CustomDomain.create!( |
| 223 | + site: pending_site, |
| 224 | + domain: 'pending-cert.com', |
| 225 | + ssl_status: :pending, |
| 226 | + ssl_expires_at: 15.days.from_now |
| 227 | + ) |
| 228 | + |
| 229 | + expiring = CustomDomain.expiring_soon |
| 230 | + assert_includes expiring, expired_soon |
| 231 | + refute_includes expiring, not_expiring |
| 232 | + refute_includes expiring, pending_cert |
| 233 | + end |
| 234 | + |
| 235 | + test 'verification_record_name' do |
| 236 | + @custom_domain.domain = 'example.com' |
| 237 | + assert_equal '_tiddlyhost-verification.example.com', @custom_domain.verification_record_name |
| 238 | + end |
| 239 | + |
| 240 | + test 'verification_record_value returns token' do |
| 241 | + @custom_domain.save! |
| 242 | + assert_equal @custom_domain.verification_token, @custom_domain.verification_record_value |
| 243 | + end |
| 244 | + |
| 245 | + test 'dns_verification_instructions contains key information' do |
| 246 | + @custom_domain.save! |
| 247 | + instructions = @custom_domain.dns_verification_instructions |
| 248 | + |
| 249 | + assert_includes instructions, @custom_domain.verification_record_name |
| 250 | + assert_includes instructions, @custom_domain.verification_token |
| 251 | + assert_includes instructions, 'TXT' |
| 252 | + assert_includes instructions, 'Verify Domain' |
| 253 | + end |
| 254 | + |
| 255 | + test 'belongs to site' do |
| 256 | + assert_equal @site, @custom_domain.site |
| 257 | + end |
| 258 | + |
| 259 | + test 'site can only have one custom domain' do |
| 260 | + @custom_domain.save! |
| 261 | + |
| 262 | + second_domain = CustomDomain.new(site: @site, domain: 'another.com') |
| 263 | + refute second_domain.valid? |
| 264 | + assert_includes second_domain.errors[:site_id], "has already been taken" |
| 265 | + end |
| 266 | + |
| 267 | + test 'custom domain is destroyed when site is destroyed' do |
| 268 | + @custom_domain.save! |
| 269 | + assert_difference 'CustomDomain.count', -1 do |
| 270 | + @site.destroy |
| 271 | + end |
| 272 | + end |
| 273 | +end |
0 commit comments