Skip to content

Commit ed35083

Browse files
cursoragentjaracursorsh
andcommitted
Add test suite and configuration for Nondisposable gem
Co-authored-by: jaracursorsh <jaracursorsh@mitomail.com>
1 parent f524345 commit ed35083

11 files changed

+438
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
/spec/reports/
88
/tmp/
99
/dist/
10+
/vendor/
1011

1112
TODO

Gemfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ source "https://rubygems.org"
66
gemspec
77

88
gem "rake", "~> 13.0"
9+
10+
group :test do
11+
gem "minitest", "~> 5.22"
12+
gem "minitest-reporters", "~> 1.6"
13+
gem "webmock", "~> 3.19"
14+
gem "sqlite3", "~> 1.4"
15+
gem "simplecov", "~> 0.22", require: false
16+
end

Gemfile.lock

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ PATH
33
specs:
44
nondisposable (0.1.0)
55
rails (>= 7.0.0)
6-
whenever (~> 1.0)
76

87
GEM
98
remote: https://rubygems.org/
@@ -78,18 +77,25 @@ GEM
7877
minitest (>= 5.1)
7978
securerandom (>= 0.3)
8079
tzinfo (~> 2.0, >= 2.0.5)
80+
addressable (2.8.7)
81+
public_suffix (>= 2.0.2, < 7.0)
82+
ansi (1.5.0)
8183
base64 (0.2.0)
8284
bigdecimal (3.1.8)
8385
builder (3.3.0)
84-
chronic (0.10.2)
8586
concurrent-ruby (1.3.4)
8687
connection_pool (2.4.1)
88+
crack (1.0.0)
89+
bigdecimal
90+
rexml
8791
crass (1.0.6)
8892
date (3.3.4)
93+
docile (1.4.1)
8994
drb (2.2.1)
9095
erubi (1.13.0)
9196
globalid (1.2.1)
9297
activesupport (>= 6.1)
98+
hashdiff (1.2.0)
9399
i18n (1.14.6)
94100
concurrent-ruby (~> 1.0)
95101
io-console (0.7.2)
@@ -108,6 +114,11 @@ GEM
108114
marcel (1.0.4)
109115
mini_mime (1.1.5)
110116
minitest (5.25.1)
117+
minitest-reporters (1.7.1)
118+
ansi
119+
builder
120+
minitest (>= 5.0)
121+
ruby-progressbar
111122
net-imap (0.5.0)
112123
date
113124
net-protocol
@@ -132,6 +143,7 @@ GEM
132143
racc (~> 1.4)
133144
psych (5.1.2)
134145
stringio
146+
public_suffix (6.0.2)
135147
racc (1.8.1)
136148
rack (3.1.8)
137149
rack-session (2.0.0)
@@ -175,19 +187,35 @@ GEM
175187
psych (>= 4.0.0)
176188
reline (0.5.10)
177189
io-console (~> 0.5)
190+
rexml (3.4.1)
191+
ruby-progressbar (1.13.0)
178192
securerandom (0.3.1)
193+
simplecov (0.22.0)
194+
docile (~> 1.1)
195+
simplecov-html (~> 0.11)
196+
simplecov_json_formatter (~> 0.1)
197+
simplecov-html (0.13.2)
198+
simplecov_json_formatter (0.1.4)
199+
sqlite3 (1.7.3-aarch64-linux)
200+
sqlite3 (1.7.3-arm-linux)
201+
sqlite3 (1.7.3-arm64-darwin)
202+
sqlite3 (1.7.3-x86-linux)
203+
sqlite3 (1.7.3-x86_64-darwin)
204+
sqlite3 (1.7.3-x86_64-linux)
179205
stringio (3.1.1)
180206
thor (1.3.2)
181207
timeout (0.4.1)
182208
tzinfo (2.0.6)
183209
concurrent-ruby (~> 1.0)
184210
useragent (0.16.10)
211+
webmock (3.25.1)
212+
addressable (>= 2.8.0)
213+
crack (>= 0.3.2)
214+
hashdiff (>= 0.4.0, < 2.0.0)
185215
webrick (1.8.2)
186216
websocket-driver (0.7.6)
187217
websocket-extensions (>= 0.1.0)
188218
websocket-extensions (0.1.5)
189-
whenever (1.0.0)
190-
chronic (>= 0.6.3)
191219
zeitwerk (2.7.1)
192220

193221
PLATFORMS
@@ -199,8 +227,13 @@ PLATFORMS
199227
x86_64-linux
200228

201229
DEPENDENCIES
230+
minitest (~> 5.22)
231+
minitest-reporters (~> 1.6)
202232
nondisposable!
203233
rake (~> 13.0)
234+
simplecov (~> 0.22)
235+
sqlite3 (~> 1.4)
236+
webmock (~> 3.19)
204237

205238
BUNDLED WITH
206239
2.5.22

Rakefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
# frozen_string_literal: true
22

33
require "bundler/gem_tasks"
4-
task default: %i[]
4+
require "rake/testtask"
5+
6+
Rake::TestTask.new(:test) do |t|
7+
t.libs << "lib"
8+
t.libs << "test"
9+
t.test_files = FileList['test/**/*_test.rb']
10+
t.warning = false
11+
end
12+
13+
task default: %i[test]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class ConfigurationTest < Minitest::Test
6+
def setup
7+
Nondisposable.configuration = Nondisposable::Configuration.new
8+
Nondisposable::DisposableDomain.delete_all
9+
end
10+
11+
def test_configuration_defaults
12+
config = Nondisposable.configuration
13+
assert_equal "provider is not allowed", config.error_message
14+
assert_equal [], config.additional_domains
15+
assert_equal [], config.excluded_domains
16+
end
17+
18+
def test_configure_block
19+
Nondisposable.configure do |c|
20+
c.error_message = "blocked"
21+
c.additional_domains = ["added.com"]
22+
c.excluded_domains = ["excluded.com"]
23+
end
24+
25+
config = Nondisposable.configuration
26+
assert_equal "blocked", config.error_message
27+
assert_equal ["added.com"], config.additional_domains
28+
assert_equal ["excluded.com"], config.excluded_domains
29+
end
30+
31+
def test_disposable_query_helper_method
32+
Nondisposable::DisposableDomain.create!(name: "bad.com")
33+
assert Nondisposable.disposable?("user@bad.com")
34+
refute Nondisposable.disposable?("user@good.com")
35+
refute Nondisposable.disposable?(nil)
36+
refute Nondisposable.disposable?("nogood")
37+
end
38+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class DisposableDomainModelTest < Minitest::Test
6+
def setup
7+
Nondisposable.configuration = Nondisposable::Configuration.new
8+
Nondisposable::DisposableDomain.delete_all
9+
end
10+
11+
def test_validates_presence_and_uniqueness_case_insensitive
12+
d1 = Nondisposable::DisposableDomain.create!(name: "TempMail.com")
13+
d2 = Nondisposable::DisposableDomain.new(name: "tempmail.com")
14+
15+
refute d2.valid?
16+
assert_includes d2.errors[:name], "has already been taken"
17+
18+
d1.update!(name: "new.com")
19+
d2.name = "tempmail.com"
20+
end
21+
22+
def test_disposable_query_with_blank
23+
refute Nondisposable::DisposableDomain.disposable?(nil)
24+
refute Nondisposable::DisposableDomain.disposable?("")
25+
end
26+
27+
def test_disposable_query_true_when_in_db
28+
Nondisposable::DisposableDomain.create!(name: "foo.com")
29+
assert Nondisposable::DisposableDomain.disposable?("foo.com")
30+
assert Nondisposable::DisposableDomain.disposable?("FOO.COM"), "should be case-insensitive"
31+
end
32+
33+
def test_disposable_query_true_when_in_additional_domains
34+
Nondisposable.configuration.additional_domains = ["bar.com"]
35+
assert Nondisposable::DisposableDomain.disposable?("bar.com")
36+
end
37+
38+
def test_disposable_query_false_when_excluded
39+
Nondisposable::DisposableDomain.create!(name: "baz.com")
40+
Nondisposable.configuration.excluded_domains = ["baz.com"]
41+
refute Nondisposable::DisposableDomain.disposable?("baz.com")
42+
end
43+
end
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class DomainListUpdaterTest < Minitest::Test
6+
def setup
7+
Nondisposable.configuration = Nondisposable::Configuration.new
8+
Nondisposable::DisposableDomain.delete_all
9+
end
10+
11+
def test_successfully_updates_domains_from_remote
12+
body = "temp.com\ntrash.net\n"
13+
stub = stub_request(:get, %r{raw\.githubusercontent\.com/.*/disposable_email_blocklist\.conf}).to_return(status: 200, body: body)
14+
15+
assert Nondisposable::DomainListUpdater.update
16+
assert_requested stub
17+
names = Nondisposable::DisposableDomain.order(:name).pluck(:name)
18+
assert_equal %w[temp.com trash.net], names
19+
end
20+
21+
def test_merges_additional_and_excluded_domains
22+
Nondisposable.configuration.additional_domains = ["extra.com", "trash.net"]
23+
Nondisposable.configuration.excluded_domains = ["trash.net"]
24+
25+
body = "temp.com\ntrash.net\n"
26+
stub_request(:get, /disposable-email-domains/).to_return(status: 200, body: body)
27+
28+
assert Nondisposable::DomainListUpdater.update
29+
names = Nondisposable::DisposableDomain.order(:name).pluck(:name)
30+
assert_equal %w[extra.com temp.com], names
31+
end
32+
33+
def test_returns_false_on_http_failure
34+
stub_request(:get, /disposable-email-domains/).to_return(status: 500, body: "")
35+
36+
refute Nondisposable::DomainListUpdater.update
37+
assert_equal 0, Nondisposable::DisposableDomain.count
38+
end
39+
40+
def test_returns_false_on_empty_list
41+
stub_request(:get, /disposable-email-domains/).to_return(status: 200, body: "\n\n")
42+
43+
refute Nondisposable::DomainListUpdater.update
44+
assert_equal 0, Nondisposable::DisposableDomain.count
45+
end
46+
47+
def test_returns_false_on_network_error
48+
stub_request(:get, /disposable-email-domains/).to_raise(SocketError.new("no network"))
49+
50+
refute Nondisposable::DomainListUpdater.update
51+
assert_equal 0, Nondisposable::DisposableDomain.count
52+
end
53+
54+
def test_transactionality_deletes_then_inserts
55+
body = "a.com\nb.com\n"
56+
stub_request(:get, /disposable-email-domains/).to_return(status: 200, body: body)
57+
58+
Nondisposable::DisposableDomain.create!(name: "old.com")
59+
assert Nondisposable::DomainListUpdater.update
60+
refute Nondisposable::DisposableDomain.exists?(name: "old.com")
61+
assert_equal %w[a.com b.com], Nondisposable::DisposableDomain.order(:name).pluck(:name)
62+
end
63+
64+
def test_update_rolls_back_on_insert_error
65+
# Pre-populate with a domain that should remain if the transaction rolls back
66+
Nondisposable::DisposableDomain.create!(name: "keep.com")
67+
68+
body = "a.com\n"
69+
stub_request(:get, /disposable-email-domains/).to_return(status: 200, body: body)
70+
71+
Nondisposable::DisposableDomain.stub(:create, proc { raise "insert failed" }) do
72+
refute Nondisposable::DomainListUpdater.update
73+
end
74+
75+
# Ensure rollback kept the original data
76+
assert_equal %w[keep.com], Nondisposable::DisposableDomain.order(:name).pluck(:name)
77+
end
78+
end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class NondisposableValidatorTest < Minitest::Test
6+
def setup
7+
Nondisposable.configuration = Nondisposable::Configuration.new
8+
Nondisposable::DisposableDomain.delete_all
9+
end
10+
11+
def test_allows_blank_values
12+
# Use a model without presence validation to isolate validator behavior
13+
klass = Class.new(ApplicationRecord) do
14+
self.table_name = "users"
15+
validates :email, nondisposable: true
16+
end
17+
user = klass.new(email: "")
18+
assert user.valid?, "Blank emails should be allowed by validator (presence handled elsewhere)"
19+
end
20+
21+
def test_allows_invalid_format_without_at_symbol
22+
user = User.new(email: "invalid-email")
23+
# The validator returns if domain is nil (no '@'), so no error should be added here
24+
assert user.valid?, "Validator should skip invalid formats without adding errors"
25+
end
26+
27+
def test_blocks_disposable_domain
28+
Nondisposable::DisposableDomain.create!(name: "trashmail.com")
29+
user = User.new(email: "foo@trashmail.com")
30+
refute user.valid?
31+
assert_includes user.errors[:email], Nondisposable.configuration.error_message
32+
end
33+
34+
def test_blocks_additional_configured_domain_even_if_not_in_db
35+
Nondisposable.configuration.additional_domains = ["tempmail.com"]
36+
user = User.new(email: "bar@tempmail.com")
37+
refute user.valid?
38+
assert_includes user.errors[:email], Nondisposable.configuration.error_message
39+
end
40+
41+
def test_allows_excluded_domains_even_if_in_db
42+
Nondisposable::DisposableDomain.create!(name: "filter.com")
43+
Nondisposable.configuration.excluded_domains = ["filter.com"]
44+
user = User.new(email: "ok@filter.com")
45+
assert user.valid?
46+
end
47+
48+
def test_custom_error_message
49+
Nondisposable::DisposableDomain.create!(name: "spam.io")
50+
custom_message = "is a disposable email address, please use a permanent email address."
51+
user = Class.new(User) do
52+
validates :email, nondisposable: { message: "is a disposable email address, please use a permanent email address." }
53+
end.new(email: "u@spam.io")
54+
55+
refute user.valid?
56+
assert_includes user.errors[:email], custom_message
57+
end
58+
59+
def test_error_handling_logs_and_adds_fallback_error
60+
# Stub out DisposableDomain.disposable? to raise and ensure fallback error is added
61+
Nondisposable::DisposableDomain.stub(:disposable?, proc { raise "boom" }) do
62+
user = User.new(email: "x@y.com")
63+
refute user.valid?
64+
assert_includes user.errors[:email], "is an invalid email address, cannot check if it's disposable"
65+
end
66+
end
67+
68+
def test_uppercase_email_is_handled
69+
Nondisposable::DisposableDomain.create!(name: "bad.com")
70+
user = User.new(email: "USER@BAD.COM")
71+
refute user.valid?
72+
end
73+
74+
def test_helper_method_validates_nondisposable_email
75+
klass = Class.new(ApplicationRecord) do
76+
self.table_name = "users"
77+
extend ActiveModel::Validations::HelperMethods
78+
validates_nondisposable_email :email
79+
end
80+
Nondisposable::DisposableDomain.create!(name: "helper.com")
81+
user = klass.new(email: "x@helper.com")
82+
refute user.valid?
83+
end
84+
end

0 commit comments

Comments
 (0)