96.99% test coverage
This commit is contained in:
@@ -61,11 +61,12 @@ end
|
|||||||
group :development do
|
group :development do
|
||||||
# Use console on exceptions pages [https://github.com/rails/web-console]
|
# Use console on exceptions pages [https://github.com/rails/web-console]
|
||||||
gem "web-console"
|
gem "web-console"
|
||||||
gem "mailcatcher"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
||||||
|
gem "benchmark", require: false
|
||||||
|
gem "simplecov", require: false
|
||||||
gem "capybara"
|
gem "capybara"
|
||||||
gem "selenium-webdriver"
|
gem "selenium-webdriver"
|
||||||
end
|
end
|
||||||
|
|||||||
+15
-56
@@ -81,6 +81,7 @@ GEM
|
|||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcrypt (3.1.21)
|
bcrypt (3.1.21)
|
||||||
bcrypt_pbkdf (1.1.2)
|
bcrypt_pbkdf (1.1.2)
|
||||||
|
benchmark (0.5.0)
|
||||||
bigdecimal (4.0.1)
|
bigdecimal (4.0.1)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.21.1)
|
bootsnap (1.21.1)
|
||||||
@@ -112,11 +113,11 @@ GEM
|
|||||||
connection_pool (3.0.2)
|
connection_pool (3.0.2)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
csv (3.3.5)
|
csv (3.3.5)
|
||||||
daemons (1.4.1)
|
|
||||||
date (3.5.1)
|
date (3.5.1)
|
||||||
debug (1.11.1)
|
debug (1.11.1)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
|
docile (1.4.1)
|
||||||
dotenv (3.2.0)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
ed25519 (1.4.0)
|
ed25519 (1.4.0)
|
||||||
@@ -124,17 +125,12 @@ GEM
|
|||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.4.0)
|
et-orbi (1.4.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
eventmachine (1.2.7)
|
|
||||||
ffi (1.17.3-x86_64-linux-gnu)
|
ffi (1.17.3-x86_64-linux-gnu)
|
||||||
fugit (1.12.1)
|
fugit (1.12.1)
|
||||||
et-orbi (~> 1.4)
|
et-orbi (~> 1.4)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.3.0)
|
globalid (1.3.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
haml (7.2.0)
|
|
||||||
temple (>= 0.8.2)
|
|
||||||
thor
|
|
||||||
tilt
|
|
||||||
htmlentities (4.4.2)
|
htmlentities (4.4.2)
|
||||||
i18n (1.14.8)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
@@ -177,16 +173,6 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
mailcatcher (0.2.4)
|
|
||||||
eventmachine
|
|
||||||
haml
|
|
||||||
i18n
|
|
||||||
json
|
|
||||||
mail
|
|
||||||
sinatra
|
|
||||||
skinny (>= 0.1.2)
|
|
||||||
sqlite3-ruby
|
|
||||||
thin
|
|
||||||
marcel (1.1.0)
|
marcel (1.1.0)
|
||||||
matrix (0.4.3)
|
matrix (0.4.3)
|
||||||
mini_magick (5.3.1)
|
mini_magick (5.3.1)
|
||||||
@@ -195,8 +181,6 @@ GEM
|
|||||||
minitest (6.0.1)
|
minitest (6.0.1)
|
||||||
prism (~> 1.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
mustermann (3.0.4)
|
|
||||||
ruby2_keywords (~> 0.0.1)
|
|
||||||
net-imap (0.6.2)
|
net-imap (0.6.2)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
@@ -236,10 +220,6 @@ GEM
|
|||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.4)
|
rack (3.2.4)
|
||||||
rack-protection (4.2.1)
|
|
||||||
base64 (>= 0.1.0)
|
|
||||||
logger (>= 1.6.0)
|
|
||||||
rack (>= 3.0.0, < 4)
|
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
@@ -325,7 +305,6 @@ GEM
|
|||||||
ruby-vips (2.3.0)
|
ruby-vips (2.3.0)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.2.2)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.40.0)
|
selenium-webdriver (4.40.0)
|
||||||
@@ -334,16 +313,12 @@ GEM
|
|||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 4.0)
|
rubyzip (>= 1.2.2, < 4.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
sinatra (4.2.1)
|
simplecov (0.22.0)
|
||||||
logger (>= 1.6.0)
|
docile (~> 1.1)
|
||||||
mustermann (~> 3.0)
|
simplecov-html (~> 0.11)
|
||||||
rack (>= 3.0.0, < 4)
|
simplecov_json_formatter (~> 0.1)
|
||||||
rack-protection (= 4.2.1)
|
simplecov-html (0.13.2)
|
||||||
rack-session (>= 2.0.0, < 3)
|
simplecov_json_formatter (0.1.4)
|
||||||
tilt (~> 2.0)
|
|
||||||
skinny (0.2.2)
|
|
||||||
eventmachine (~> 1.0)
|
|
||||||
thin
|
|
||||||
solid_cable (3.0.12)
|
solid_cable (3.0.12)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
@@ -361,8 +336,6 @@ GEM
|
|||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
thor (>= 1.3.1)
|
thor (>= 1.3.1)
|
||||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||||
sqlite3-ruby (1.3.3)
|
|
||||||
sqlite3 (>= 1.3.3)
|
|
||||||
sshkit (1.25.0)
|
sshkit (1.25.0)
|
||||||
base64
|
base64
|
||||||
logger
|
logger
|
||||||
@@ -373,15 +346,8 @@ GEM
|
|||||||
stimulus-rails (1.3.4)
|
stimulus-rails (1.3.4)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.2.0)
|
stringio (3.2.0)
|
||||||
temple (0.10.4)
|
|
||||||
thin (2.0.1)
|
|
||||||
daemons (~> 1.0, >= 1.0.9)
|
|
||||||
eventmachine (~> 1.0, >= 1.0.4)
|
|
||||||
logger
|
|
||||||
rack (>= 1, < 4)
|
|
||||||
thor (1.5.0)
|
thor (1.5.0)
|
||||||
thruster (0.1.17-x86_64-linux)
|
thruster (0.1.17-x86_64-linux)
|
||||||
tilt (2.7.0)
|
|
||||||
timeout (0.6.0)
|
timeout (0.6.0)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
turbo-rails (2.0.23)
|
turbo-rails (2.0.23)
|
||||||
@@ -413,6 +379,7 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bcrypt (~> 3.1.7)
|
bcrypt (~> 3.1.7)
|
||||||
|
benchmark
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
bundler-audit
|
bundler-audit
|
||||||
@@ -424,13 +391,13 @@ DEPENDENCIES
|
|||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
kamal
|
kamal
|
||||||
mailcatcher
|
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.1.2)
|
rails (~> 8.1.2)
|
||||||
roo (~> 3.0)
|
roo (~> 3.0)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
|
simplecov
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
@@ -459,6 +426,7 @@ CHECKSUMS
|
|||||||
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||||
bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf
|
bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf
|
||||||
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
|
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
|
||||||
|
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
|
||||||
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
|
||||||
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
|
||||||
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
|
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
|
||||||
@@ -472,20 +440,18 @@ CHECKSUMS
|
|||||||
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
||||||
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
|
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
|
||||||
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
|
||||||
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
|
|
||||||
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
||||||
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
|
||||||
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
||||||
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
||||||
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||||
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
|
||||||
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
|
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
|
||||||
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||||
et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc
|
et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc
|
||||||
eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972
|
|
||||||
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
|
||||||
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
|
||||||
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
|
||||||
haml (7.2.0) sha256=87fd2b71f7feab1724337b090a7d767f5ab2d42f08c974f3ead673f18cfcd55a
|
|
||||||
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
||||||
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
||||||
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
|
||||||
@@ -500,14 +466,12 @@ CHECKSUMS
|
|||||||
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
||||||
loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6
|
loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6
|
||||||
mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
|
mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
|
||||||
mailcatcher (0.2.4) sha256=ba1d6f23d32f69929dce332d0aa7aeabddadd15507de474754e593807111dda9
|
|
||||||
marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee
|
marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee
|
||||||
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
|
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
|
||||||
mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
|
mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
|
||||||
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||||
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
|
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
|
||||||
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
|
||||||
mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22
|
|
||||||
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
|
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
|
||||||
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||||
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||||
@@ -530,7 +494,6 @@ CHECKSUMS
|
|||||||
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
|
||||||
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||||
rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
|
rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
|
||||||
rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
|
|
||||||
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
|
||||||
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
||||||
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||||
@@ -552,25 +515,21 @@ CHECKSUMS
|
|||||||
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
|
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
|
||||||
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||||
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
|
||||||
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
|
||||||
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
||||||
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||||
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
|
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
|
||||||
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
||||||
skinny (0.2.2) sha256=f40caceccfe3e1d9826f60195a090f43ea7c1130c36b3170887db69a9fb52102
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
||||||
|
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
|
||||||
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
|
solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460
|
||||||
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
|
solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41
|
||||||
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
|
solid_queue (1.3.1) sha256=d9580111180c339804ff1a810a7768f69f5dc694d31e86cf1535ff2cd7a87428
|
||||||
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
|
sqlite3 (2.9.0-x86_64-linux-gnu) sha256=72fff9bd750070ba3af695511ba5f0e0a2d8a9206f84869640b3e99dfaf3d5a5
|
||||||
sqlite3-ruby (1.3.3) sha256=140b6742875dd5afc3f30ab95720fe60d38e154ae1f4d0728e250778a04094e7
|
|
||||||
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744
|
||||||
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
|
||||||
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
||||||
temple (0.10.4) sha256=b7a1e94b6f09038ab0b6e4fe0126996055da2c38bec53a8a336f075748fff72c
|
|
||||||
thin (2.0.1) sha256=5bbde5648377f5c3864b5da7cd89a23b5c2d8d8bb9435719f6db49644bcdade9
|
|
||||||
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||||
thruster (0.1.17-x86_64-linux) sha256=77b8f335075bd4ece7631dc84a19a710a1e6e7102cbce147b165b45851bdfcd3
|
thruster (0.1.17-x86_64-linux) sha256=77b8f335075bd4ece7631dc84a19a710a1e6e7102cbce147b165b45851bdfcd3
|
||||||
tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3
|
|
||||||
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
|
timeout (0.6.0) sha256=6d722ad619f96ee383a0c557ec6eb8c4ecb08af3af62098a0be5057bf00de1af
|
||||||
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||||
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Rails.application.configure do
|
|||||||
|
|
||||||
# Show full error reports.
|
# Show full error reports.
|
||||||
config.consider_all_requests_local = true
|
config.consider_all_requests_local = true
|
||||||
config.cache_store = :null_store
|
config.cache_store = :memory_store
|
||||||
|
|
||||||
# Render exception templates for rescuable exceptions and raise for other exceptions.
|
# Render exception templates for rescuable exceptions and raise for other exceptions.
|
||||||
config.action_dispatch.show_exceptions = :rescuable
|
config.action_dispatch.show_exceptions = :rescuable
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
require "test_helper"
|
||||||
|
require "capybara/rails"
|
||||||
|
require "capybara/minitest"
|
||||||
|
|
||||||
|
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||||
|
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
|
||||||
|
|
||||||
|
# Helper to login as a user
|
||||||
|
def login_as(user)
|
||||||
|
visit login_path
|
||||||
|
fill_in "Email", with: user.email
|
||||||
|
fill_in "Password", with: "password123456"
|
||||||
|
click_button "Sign In"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -140,4 +140,404 @@ class Admin::InvitationsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert_redirected_to root_path
|
assert_redirected_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Resend action tests
|
||||||
|
test "should resend invitation for pending invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
original_token = pending_user.invitation_token
|
||||||
|
original_sent_at = pending_user.invitation_sent_at
|
||||||
|
|
||||||
|
assert_enqueued_emails 1 do
|
||||||
|
put resend_admin_invitation_path(pending_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
assert_match /Invitation resent to #{pending_user.email}/, flash[:notice]
|
||||||
|
|
||||||
|
pending_user.reload
|
||||||
|
assert_not_equal original_token, pending_user.invitation_token
|
||||||
|
assert_operator pending_user.invitation_sent_at, :>, original_sent_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not resend accepted invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
accepted_user = users(:contributor_user)
|
||||||
|
|
||||||
|
assert_enqueued_emails 0 do
|
||||||
|
put resend_admin_invitation_path(accepted_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
assert_equal "Cannot resend an accepted invitation.", flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should update invitation_sent_at when resending" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
|
||||||
|
freeze_time do
|
||||||
|
put resend_admin_invitation_path(pending_user)
|
||||||
|
|
||||||
|
pending_user.reload
|
||||||
|
assert_in_delta Time.current.to_i, pending_user.invitation_sent_at.to_i, 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should generate new token when resending" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
original_token = pending_user.invitation_token
|
||||||
|
|
||||||
|
put resend_admin_invitation_path(pending_user)
|
||||||
|
|
||||||
|
pending_user.reload
|
||||||
|
assert_not_nil pending_user.invitation_token
|
||||||
|
assert_not_equal original_token, pending_user.invitation_token
|
||||||
|
assert pending_user.invitation_token.length >= 32
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not allow non-admin to resend invitation" do
|
||||||
|
login_as(users(:contributor_user))
|
||||||
|
|
||||||
|
put resend_admin_invitation_path(users(:pending_invitation))
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should require authentication to resend invitation" do
|
||||||
|
put resend_admin_invitation_path(users(:pending_invitation))
|
||||||
|
|
||||||
|
assert_redirected_to login_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Index action tests
|
||||||
|
test "should list pending invitations in descending order by sent date" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
# Create multiple pending invitations
|
||||||
|
user1 = User.create!(email: "user1@example.com", name: "User 1", password: "temp123456789")
|
||||||
|
user1.invite_by!(users(:admin_user))
|
||||||
|
|
||||||
|
user2 = User.create!(email: "user2@example.com", name: "User 2", password: "temp123456789")
|
||||||
|
user2.update_columns(invitation_token: "token2", invitation_sent_at: 1.hour.ago)
|
||||||
|
|
||||||
|
get admin_invitations_path
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select "h3", /Pending Invitations/i
|
||||||
|
# Check both emails appear in the page
|
||||||
|
assert_select "td", text: "user1@example.com"
|
||||||
|
assert_select "td", text: "user2@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should display accepted invitations section" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
get admin_invitations_path
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select "h3", /Recently Accepted/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show pending invitation count badge" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
get admin_invitations_path
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select "span.bg-yellow-100"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should display pending invitations table when invitations exist" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
get admin_invitations_path
|
||||||
|
|
||||||
|
assert_select "table"
|
||||||
|
assert_select "th", text: "Email"
|
||||||
|
assert_select "th", text: "Role"
|
||||||
|
assert_select "th", text: "Sent"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create action - additional tests
|
||||||
|
test "should create invitation with reviewer role" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "reviewer@example.com",
|
||||||
|
name: "New Reviewer",
|
||||||
|
role: "reviewer",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "reviewer@example.com")
|
||||||
|
assert_equal "reviewer", new_user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should create invitation with admin role" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "admin@example.com",
|
||||||
|
name: "New Admin",
|
||||||
|
role: "admin",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "admin@example.com")
|
||||||
|
assert_equal "admin", new_user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should ignore invalid role parameter" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "superadmin",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_equal "contributor", new_user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should set invited_by to current admin" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_equal users(:admin_user).id, new_user.invited_by_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should generate random password for new invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert new_user.password_digest.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not create invitation with duplicate email" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
existing_user = users(:admin_user)
|
||||||
|
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: existing_user.email,
|
||||||
|
name: "Duplicate User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_select "div.text-red-700", text: /already been taken/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should create invitation even with blank name" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
assert_difference("User.count", 1) do
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_not_nil new_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show success message with email after creating invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
assert_match /Invitation sent to newuser@example\.com/, flash[:notice]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Destroy action - additional tests
|
||||||
|
test "should show success message after cancelling invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
delete admin_invitation_path(users(:pending_invitation))
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
assert_equal "Invitation cancelled.", flash[:notice]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should delete user record when cancelling invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
user_id = pending_user.id
|
||||||
|
|
||||||
|
delete admin_invitation_path(pending_user)
|
||||||
|
|
||||||
|
assert_nil User.find_by(id: user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not allow destroying user with entries" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
user_with_entries = users(:contributor_user)
|
||||||
|
|
||||||
|
# This user has accepted invitation and potentially has entries
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
delete admin_invitation_path(user_with_entries)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
assert_equal "Cannot cancel an accepted invitation.", flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Security tests
|
||||||
|
test "should only permit email, name, primary_language, and role params" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en",
|
||||||
|
password: "hackedpassword123",
|
||||||
|
invitation_token: "hacked_token",
|
||||||
|
invitation_accepted_at: Time.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_not_nil new_user
|
||||||
|
# Password should be randomly generated, not from params
|
||||||
|
assert_not new_user.authenticate("hackedpassword123")
|
||||||
|
# Invitation token should be generated by invite_by, not from params
|
||||||
|
assert_not_equal "hacked_token", new_user.invitation_token
|
||||||
|
# Invitation should not be pre-accepted
|
||||||
|
assert_nil new_user.invitation_accepted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should require admin role for all actions" do
|
||||||
|
login_as(users(:reviewer_user))
|
||||||
|
|
||||||
|
# Index
|
||||||
|
get admin_invitations_path
|
||||||
|
assert_redirected_to root_path
|
||||||
|
|
||||||
|
# New
|
||||||
|
get new_admin_invitation_path
|
||||||
|
assert_redirected_to root_path
|
||||||
|
|
||||||
|
# Create
|
||||||
|
post admin_invitations_path, params: { user: { email: "test@example.com", name: "Test" } }
|
||||||
|
assert_redirected_to root_path
|
||||||
|
|
||||||
|
# Resend
|
||||||
|
put resend_admin_invitation_path(users(:pending_invitation))
|
||||||
|
assert_redirected_to root_path
|
||||||
|
|
||||||
|
# Destroy
|
||||||
|
delete admin_invitation_path(users(:pending_invitation))
|
||||||
|
assert_redirected_to root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Edge cases
|
||||||
|
test "should handle resending expired invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
pending_user.update_columns(invitation_sent_at: 20.days.ago)
|
||||||
|
|
||||||
|
put resend_admin_invitation_path(pending_user)
|
||||||
|
|
||||||
|
assert_redirected_to admin_invitations_path
|
||||||
|
pending_user.reload
|
||||||
|
assert pending_user.invitation_sent_at > 1.hour.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should normalize email to lowercase when creating invitation" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "NewUser@Example.COM",
|
||||||
|
name: "New User",
|
||||||
|
role: "contributor",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_not_nil new_user
|
||||||
|
assert_equal "newuser@example.com", new_user.email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle missing role parameter gracefully" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_not_nil new_user
|
||||||
|
assert_equal "contributor", new_user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle blank role parameter" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
post admin_invitations_path, params: {
|
||||||
|
user: {
|
||||||
|
email: "newuser@example.com",
|
||||||
|
name: "New User",
|
||||||
|
role: "",
|
||||||
|
primary_language: "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user = User.find_by(email: "newuser@example.com")
|
||||||
|
assert_not_nil new_user
|
||||||
|
assert_equal "contributor", new_user.role
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ class Admin::RequestsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_select "h1", "Entry Request Details"
|
assert_select "h1", "Entry Request Details"
|
||||||
assert_match @requested_entry.fi, response.body
|
assert_select "div", text: @requested_entry.fi
|
||||||
assert_match @requested_entry.en, response.body
|
assert_select "div", text: @requested_entry.en
|
||||||
assert_match @requested_entry.requested_by.name, response.body
|
assert_select "span", text: @requested_entry.requested_by.name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "should show edit form for requested entry" do
|
test "should show edit form for requested entry" do
|
||||||
|
|||||||
@@ -18,6 +18,26 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should filter users by role" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
get admin_users_path, params: { role: "reviewer" }
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select "td", text: /#{Regexp.escape(users(:reviewer_user).email)}/
|
||||||
|
assert_select "td", text: /#{Regexp.escape(users(:contributor_user).email)}/, count: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should filter users by email query" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
|
get admin_users_path, params: { q: "admin" }
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_select "td", text: /#{Regexp.escape(users(:admin_user).email)}/
|
||||||
|
assert_select "td", text: /#{Regexp.escape(users(:contributor_user).email)}/, count: 0
|
||||||
|
end
|
||||||
|
|
||||||
test "should get edit page for user when logged in as admin" do
|
test "should get edit page for user when logged in as admin" do
|
||||||
login_as(users(:admin_user))
|
login_as(users(:admin_user))
|
||||||
get edit_admin_user_path(users(:contributor_user))
|
get edit_admin_user_path(users(:contributor_user))
|
||||||
@@ -35,6 +55,45 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "reviewer", users(:contributor_user).reload.role
|
assert_equal "reviewer", users(:contributor_user).reload.role
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should not allow admin to update own role" do
|
||||||
|
admin_user = users(:admin_user)
|
||||||
|
login_as(admin_user)
|
||||||
|
|
||||||
|
patch admin_user_path(admin_user), params: {
|
||||||
|
user: { role: "reviewer" }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_users_path
|
||||||
|
assert_equal "You cannot modify your own role.", flash[:alert]
|
||||||
|
assert_equal "admin", admin_user.reload.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should ignore invalid role updates" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
contributor = users(:contributor_user)
|
||||||
|
|
||||||
|
patch admin_user_path(contributor), params: {
|
||||||
|
user: { role: "invalid_role", name: "Updated Name" }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_users_path
|
||||||
|
contributor.reload
|
||||||
|
assert_equal "contributor", contributor.role
|
||||||
|
assert_equal "Updated Name", contributor.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should render edit when update is invalid" do
|
||||||
|
login_as(users(:admin_user))
|
||||||
|
contributor = users(:contributor_user)
|
||||||
|
|
||||||
|
patch admin_user_path(contributor), params: {
|
||||||
|
user: { email: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_select "li", text: "Email can't be blank"
|
||||||
|
end
|
||||||
|
|
||||||
test "should delete user when logged in as admin" do
|
test "should delete user when logged in as admin" do
|
||||||
login_as(users(:admin_user))
|
login_as(users(:admin_user))
|
||||||
|
|
||||||
@@ -46,6 +105,37 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_redirected_to admin_users_path
|
assert_redirected_to admin_users_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should not allow admin to delete own account" do
|
||||||
|
admin_user = users(:admin_user)
|
||||||
|
login_as(admin_user)
|
||||||
|
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
delete admin_user_path(admin_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_users_path
|
||||||
|
assert_equal "You cannot delete your own account.", flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not allow deleting first admin user" do
|
||||||
|
other_admin = User.create!(
|
||||||
|
email: "other-admin@example.com",
|
||||||
|
name: "Other Admin",
|
||||||
|
role: :admin,
|
||||||
|
primary_language: "en",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_accepted_at: Time.current
|
||||||
|
)
|
||||||
|
login_as(other_admin)
|
||||||
|
|
||||||
|
assert_no_difference("User.count") do
|
||||||
|
delete admin_user_path(User.first)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to admin_users_path
|
||||||
|
assert_equal "Cannot delete the first admin user (system default contact).", flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
test "should not allow non-admin to update user" do
|
test "should not allow non-admin to update user" do
|
||||||
login_as(users(:contributor_user))
|
login_as(users(:contributor_user))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
require "test_helper"
|
||||||
|
require "roo"
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
|
class EntriesControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@entry = entries(:one)
|
||||||
|
@user = users(:admin_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# INDEX tests
|
||||||
|
test "should get index" do
|
||||||
|
get entries_url
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should filter by language" do
|
||||||
|
get entries_url, params: { language: "fi" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "input[type=hidden][name='language'][value='fi']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should filter by category" do
|
||||||
|
get entries_url, params: { category: "word" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "input[type=hidden][name='category'][value='word']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should search with query" do
|
||||||
|
get entries_url, params: { q: "test" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "input[name='q'][value='test']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should filter by starts_with" do
|
||||||
|
get entries_url, params: { starts_with: "a" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "input[type=hidden][name='starts_with'][value='a']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should paginate results" do
|
||||||
|
get entries_url, params: { page: 2 }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "div", text: /Page 2 of/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle invalid language code" do
|
||||||
|
get entries_url, params: { language: "invalid" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "input[name='language']", count: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should respond to turbo_stream" do
|
||||||
|
get entries_url, as: :turbo_stream
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should only show active entries in index" do
|
||||||
|
# Create a requested entry that should not appear
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
fi: "Requested",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: @user
|
||||||
|
)
|
||||||
|
|
||||||
|
get entries_url
|
||||||
|
assert_response :success
|
||||||
|
assert_select "td", text: requested_entry.fi, count: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# SHOW tests
|
||||||
|
test "should show entry" do
|
||||||
|
get entry_url(@entry)
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show entry with comments" do
|
||||||
|
get entry_url(@entry)
|
||||||
|
assert_response :success
|
||||||
|
assert_select "p", text: @entry.fi
|
||||||
|
end
|
||||||
|
|
||||||
|
# EDIT tests
|
||||||
|
test "should get edit" do
|
||||||
|
get edit_entry_url(@entry)
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
# UPDATE tests
|
||||||
|
test "should update entry" do
|
||||||
|
patch entry_url(@entry), params: {
|
||||||
|
entry: {
|
||||||
|
fi: "Updated Finnish",
|
||||||
|
en: "Updated English",
|
||||||
|
category: "word"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_redirected_to entry_url(@entry)
|
||||||
|
@entry.reload
|
||||||
|
assert_equal "Updated Finnish", @entry.fi
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not update entry with invalid data" do
|
||||||
|
patch entry_url(@entry), params: {
|
||||||
|
entry: {
|
||||||
|
fi: "",
|
||||||
|
en: "",
|
||||||
|
sv: "",
|
||||||
|
no: "",
|
||||||
|
ru: "",
|
||||||
|
de: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should update entry category" do
|
||||||
|
patch entry_url(@entry), params: {
|
||||||
|
entry: { category: "phrase" }
|
||||||
|
}
|
||||||
|
assert_redirected_to entry_url(@entry)
|
||||||
|
@entry.reload
|
||||||
|
assert_equal "phrase", @entry.category
|
||||||
|
end
|
||||||
|
|
||||||
|
# DOWNLOAD tests
|
||||||
|
test "should download xlsx" do
|
||||||
|
get download_entries_url(format: :xlsx)
|
||||||
|
assert_response :success
|
||||||
|
assert_equal "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
response.media_type
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should include active entries in xlsx" do
|
||||||
|
unique_entry = Entry.create!(
|
||||||
|
fi: "UniqueActiveEntry123",
|
||||||
|
category: :word,
|
||||||
|
status: :active
|
||||||
|
)
|
||||||
|
|
||||||
|
get download_entries_url(format: :xlsx)
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
Tempfile.create([ "entries", ".xlsx" ]) do |file|
|
||||||
|
file.binmode
|
||||||
|
file.write(response.body)
|
||||||
|
file.flush
|
||||||
|
|
||||||
|
sheet = Roo::Excelx.new(file.path).sheet(0)
|
||||||
|
cell_values = sheet.to_a.flatten.compact
|
||||||
|
assert_includes cell_values, unique_entry.fi
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Statistics tests
|
||||||
|
test "should calculate entry statistics" do
|
||||||
|
get entries_url
|
||||||
|
assert_response :success
|
||||||
|
assert_select "div", text: /entries/i
|
||||||
|
assert_select "div", text: /% complete/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should calculate language completion" do
|
||||||
|
get entries_url
|
||||||
|
assert_response :success
|
||||||
|
assert_select "div", text: /% complete/i
|
||||||
|
end
|
||||||
|
|
||||||
|
# Language ordering tests
|
||||||
|
test "should prioritize selected language in display" do
|
||||||
|
get entries_url, params: { language: "fi" }
|
||||||
|
assert_response :success
|
||||||
|
assert_select "th span", text: "FI", count: 1
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -90,4 +90,306 @@ class InvitationsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_redirected_to root_path
|
assert_redirected_to root_path
|
||||||
assert_equal "Invalid or expired invitation link.", flash[:alert]
|
assert_equal "Invalid or expired invitation link.", flash[:alert]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should redirect admin to dashboard after accepting invitation" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
pending_admin = User.create!(
|
||||||
|
email: "pending-admin@example.com",
|
||||||
|
name: "Pending Admin",
|
||||||
|
role: :admin,
|
||||||
|
primary_language: "en",
|
||||||
|
invitation_token: "pending_admin_token",
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invited_by: inviter,
|
||||||
|
password: "password123456"
|
||||||
|
)
|
||||||
|
|
||||||
|
patch accept_invitation_path(pending_admin.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_admin.reload
|
||||||
|
assert_not_nil pending_admin.invitation_accepted_at
|
||||||
|
assert_nil pending_admin.invitation_token
|
||||||
|
assert_equal pending_admin.id, session[:user_id]
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Entry activation tests
|
||||||
|
test "should activate approved entries on invitation acceptance" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
# Create approved entry for this user
|
||||||
|
approved_entry = Entry.create!(
|
||||||
|
category: :word,
|
||||||
|
fi: "Test word",
|
||||||
|
status: :approved,
|
||||||
|
requested_by: user
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create requested entry (should not be activated)
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
category: :word,
|
||||||
|
fi: "Requested word",
|
||||||
|
status: :requested,
|
||||||
|
requested_by: user
|
||||||
|
)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approved_entry.reload
|
||||||
|
requested_entry.reload
|
||||||
|
|
||||||
|
assert_equal "active", approved_entry.status
|
||||||
|
assert_equal "requested", requested_entry.status
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should activate multiple approved entries on invitation acceptance" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
# Create multiple approved entries
|
||||||
|
entries = 3.times.map do |i|
|
||||||
|
Entry.create!(
|
||||||
|
category: :word,
|
||||||
|
fi: "Word #{i}",
|
||||||
|
status: :approved,
|
||||||
|
requested_by: user
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.each do |entry|
|
||||||
|
entry.reload
|
||||||
|
assert_equal "active", entry.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not activate entries for other users" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
other_user = users(:admin_user)
|
||||||
|
|
||||||
|
# Create approved entry for another user
|
||||||
|
other_entry = Entry.create!(
|
||||||
|
category: :word,
|
||||||
|
fi: "Other user word",
|
||||||
|
status: :approved,
|
||||||
|
requested_by: other_user
|
||||||
|
)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
other_entry.reload
|
||||||
|
assert_equal "approved", other_entry.status
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle invitation acceptance with no entries" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
# Ensure the user has no entries
|
||||||
|
Entry.where(requested_by: user).delete_all
|
||||||
|
assert_equal 0, Entry.where(requested_by: user).count
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
user.reload
|
||||||
|
assert_not_nil user.invitation_accepted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
# Security tests
|
||||||
|
test "should only permit password parameters" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
original_email = user.email
|
||||||
|
original_name = user.name
|
||||||
|
original_role = user.role
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123",
|
||||||
|
email: "hacker@example.com",
|
||||||
|
name: "Hacker Name",
|
||||||
|
role: "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert_equal original_email, user.email
|
||||||
|
assert_equal original_name, user.name
|
||||||
|
assert_equal original_role, user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not log in user when password validation fails" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "short",
|
||||||
|
password_confirmation: "short"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Edge cases
|
||||||
|
test "should handle already accepted invitation" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
user.update!(invitation_accepted_at: Time.current, invitation_token: nil)
|
||||||
|
|
||||||
|
patch accept_invitation_path("some_token"), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
assert_equal "Invalid or expired invitation link.", flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should set invitation_accepted_at timestamp" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
assert_nil user.invitation_accepted_at
|
||||||
|
|
||||||
|
freeze_time do
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert_in_delta Time.current.to_i, user.invitation_accepted_at.to_i, 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should clear invitation token after acceptance" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
token = user.invitation_token
|
||||||
|
assert_not_nil token
|
||||||
|
|
||||||
|
patch accept_invitation_path(token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert_nil user.invitation_token
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should require password to be present" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: nil,
|
||||||
|
password_confirmation: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# has_secure_password validates password presence when setting password
|
||||||
|
user.reload
|
||||||
|
assert_nil user.invitation_accepted_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show validation errors for short password" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "short",
|
||||||
|
password_confirmation: "short"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_select "div.text-red-700", text: /too short/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show validation errors for mismatched passwords" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "differentpassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_select "div.text-red-700", text: /confirmation/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should authenticate with new password after acceptance" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "mynewpassword123",
|
||||||
|
password_confirmation: "mynewpassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert user.authenticate("mynewpassword123")
|
||||||
|
assert_not user.authenticate("oldpassword")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should log in user immediately after acceptance" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
# First visit the invitation page to establish session
|
||||||
|
get invitation_path(user.invitation_token)
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show welcome message with user name" do
|
||||||
|
user = users(:pending_invitation)
|
||||||
|
|
||||||
|
patch accept_invitation_path(user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "securepassword123",
|
||||||
|
password_confirmation: "securepassword123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_match /Welcome to Sanasto Wiki, #{user.name}!/, flash[:notice]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@user = users(:admin_user)
|
||||||
|
@user.update!(invitation_accepted_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
# NEW tests
|
||||||
|
test "should get new" do
|
||||||
|
get new_password_reset_url
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should show password reset form" do
|
||||||
|
get new_password_reset_url
|
||||||
|
assert_select "form"
|
||||||
|
assert_select "input[type=email]"
|
||||||
|
end
|
||||||
|
|
||||||
|
# CREATE tests
|
||||||
|
test "should send reset email for existing user" do
|
||||||
|
assert_enqueued_emails 1 do
|
||||||
|
post password_resets_url, params: { email: @user.email }
|
||||||
|
end
|
||||||
|
assert_redirected_to login_url
|
||||||
|
assert_match /password reset instructions/i, flash[:notice]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should generate reset token for existing user" do
|
||||||
|
post password_resets_url, params: { email: @user.email }
|
||||||
|
@user.reload
|
||||||
|
assert_not_nil @user.reset_password_token
|
||||||
|
assert_not_nil @user.reset_password_sent_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle non-existent email gracefully" do
|
||||||
|
post password_resets_url, params: { email: "nonexistent@example.com" }
|
||||||
|
assert_redirected_to login_url
|
||||||
|
assert_match /if that email address is in our system/i, flash[:notice]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should send invitation for user without accepted invitation" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
assert_nil pending_user.invitation_accepted_at
|
||||||
|
|
||||||
|
assert_enqueued_emails 1 do
|
||||||
|
post password_resets_url, params: { email: pending_user.email }
|
||||||
|
end
|
||||||
|
assert_redirected_to login_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle blank email" do
|
||||||
|
post password_resets_url, params: { email: "" }
|
||||||
|
assert_redirected_to login_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should handle email with whitespace" do
|
||||||
|
post password_resets_url, params: { email: " #{@user.email} " }
|
||||||
|
@user.reload
|
||||||
|
assert_not_nil @user.reset_password_token
|
||||||
|
end
|
||||||
|
|
||||||
|
# EDIT tests
|
||||||
|
test "should show reset password form with valid token" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
get edit_password_reset_url(@user.reset_password_token)
|
||||||
|
assert_response :success
|
||||||
|
assert_select "form"
|
||||||
|
assert_select "input[type=password]", count: 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject invalid token" do
|
||||||
|
get edit_password_reset_url("invalid_token")
|
||||||
|
assert_redirected_to login_url
|
||||||
|
assert_match /invalid/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject expired token" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: 2.hours.ago
|
||||||
|
)
|
||||||
|
get edit_password_reset_url(@user.reset_password_token)
|
||||||
|
assert_redirected_to new_password_reset_url
|
||||||
|
assert_match /expired/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
# UPDATE tests
|
||||||
|
test "should reset password with valid token" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_url
|
||||||
|
assert_match /password has been reset/i, flash[:notice]
|
||||||
|
|
||||||
|
@user.reload
|
||||||
|
assert_nil @user.reset_password_token
|
||||||
|
assert_nil @user.reset_password_sent_at
|
||||||
|
assert @user.authenticate("newpassword12345")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should auto-login after successful password reset" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject mismatched passwords" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "differentpassword"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_match /doesn't match/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject blank password" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "",
|
||||||
|
password_confirmation: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_match /cannot be blank/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject expired token on update" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: 2.hours.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to new_password_reset_url
|
||||||
|
assert_match /expired/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should reject invalid token on update" do
|
||||||
|
patch password_reset_url("invalid_token"), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to login_url
|
||||||
|
assert_match /invalid/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should enforce password validations" do
|
||||||
|
@user.update!(
|
||||||
|
reset_password_token: SecureRandom.urlsafe_base64(32),
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
# Password too short (less than 12 characters)
|
||||||
|
patch password_reset_url(@user.reset_password_token), params: {
|
||||||
|
password: "short",
|
||||||
|
password_confirmation: "short"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class EntriesHelperTest < ActionView::TestCase
|
||||||
|
setup do
|
||||||
|
@entry = entries(:one)
|
||||||
|
end
|
||||||
|
|
||||||
|
# alphabet_letters tests
|
||||||
|
test "alphabet_letters returns A-Z for blank language code" do
|
||||||
|
result = alphabet_letters(nil)
|
||||||
|
assert_equal ("A".."Z").to_a, result
|
||||||
|
assert_equal 26, result.length
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z for empty string" do
|
||||||
|
result = alphabet_letters("")
|
||||||
|
assert_equal ("A".."Z").to_a, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z for unknown language" do
|
||||||
|
result = alphabet_letters("unknown")
|
||||||
|
assert_equal ("A".."Z").to_a, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z for English" do
|
||||||
|
result = alphabet_letters("en")
|
||||||
|
assert_equal ("A".."Z").to_a, result
|
||||||
|
assert_equal 26, result.length
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z for German" do
|
||||||
|
result = alphabet_letters("de")
|
||||||
|
assert_equal ("A".."Z").to_a, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns Cyrillic alphabet for Russian" do
|
||||||
|
result = alphabet_letters("ru")
|
||||||
|
assert_equal 33, result.length
|
||||||
|
assert_includes result, "А"
|
||||||
|
assert_includes result, "Я"
|
||||||
|
assert_includes result, "Ё"
|
||||||
|
assert_not_includes result, "A"
|
||||||
|
assert_not_includes result, "Z"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z plus Æ Ø Å for Norwegian" do
|
||||||
|
result = alphabet_letters("no")
|
||||||
|
assert_equal 29, result.length
|
||||||
|
assert_includes result, "A"
|
||||||
|
assert_includes result, "Z"
|
||||||
|
assert_includes result, "Æ"
|
||||||
|
assert_includes result, "Ø"
|
||||||
|
assert_includes result, "Å"
|
||||||
|
assert_equal [ "Æ", "Ø", "Å" ], result.last(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z plus Å Ä Ö for Swedish" do
|
||||||
|
result = alphabet_letters("sv")
|
||||||
|
assert_equal 29, result.length
|
||||||
|
assert_includes result, "A"
|
||||||
|
assert_includes result, "Z"
|
||||||
|
assert_includes result, "Å"
|
||||||
|
assert_includes result, "Ä"
|
||||||
|
assert_includes result, "Ö"
|
||||||
|
assert_equal [ "Å", "Ä", "Ö" ], result.last(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabet_letters returns A-Z plus Å Ä Ö for Finnish" do
|
||||||
|
result = alphabet_letters("fi")
|
||||||
|
assert_equal 29, result.length
|
||||||
|
assert_includes result, "A"
|
||||||
|
assert_includes result, "Z"
|
||||||
|
assert_includes result, "Å"
|
||||||
|
assert_includes result, "Ä"
|
||||||
|
assert_includes result, "Ö"
|
||||||
|
assert_equal [ "Å", "Ä", "Ö" ], result.last(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
# entry_translation_for tests
|
||||||
|
test "entry_translation_for returns Finnish translation" do
|
||||||
|
@entry.fi = "Suomalainen sana"
|
||||||
|
result = entry_translation_for(@entry, "fi")
|
||||||
|
assert_equal "Suomalainen sana", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns English translation" do
|
||||||
|
@entry.en = "English word"
|
||||||
|
result = entry_translation_for(@entry, "en")
|
||||||
|
assert_equal "English word", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns Swedish translation" do
|
||||||
|
@entry.sv = "Svenskt ord"
|
||||||
|
result = entry_translation_for(@entry, "sv")
|
||||||
|
assert_equal "Svenskt ord", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns Norwegian translation" do
|
||||||
|
@entry.no = "Norsk ord"
|
||||||
|
result = entry_translation_for(@entry, "no")
|
||||||
|
assert_equal "Norsk ord", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns Russian translation" do
|
||||||
|
@entry.ru = "Русское слово"
|
||||||
|
result = entry_translation_for(@entry, "ru")
|
||||||
|
assert_equal "Русское слово", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns German translation" do
|
||||||
|
@entry.de = "Deutsches Wort"
|
||||||
|
result = entry_translation_for(@entry, "de")
|
||||||
|
assert_equal "Deutsches Wort", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns nil for invalid language code" do
|
||||||
|
result = entry_translation_for(@entry, "invalid")
|
||||||
|
assert_nil result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns nil for non-existent attribute" do
|
||||||
|
result = entry_translation_for(@entry, "zz")
|
||||||
|
assert_nil result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for returns nil for blank translation" do
|
||||||
|
@entry.fi = nil
|
||||||
|
result = entry_translation_for(@entry, "fi")
|
||||||
|
assert_nil result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry_translation_for handles symbol language code" do
|
||||||
|
@entry.fi = "Test"
|
||||||
|
result = entry_translation_for(@entry, :fi)
|
||||||
|
assert_equal "Test", result
|
||||||
|
end
|
||||||
|
|
||||||
|
# format_entry_category tests
|
||||||
|
test "format_entry_category formats word category" do
|
||||||
|
@entry.category = "word"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Word", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_category formats phrase category" do
|
||||||
|
@entry.category = "phrase"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Phrase", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_category formats proper_name category" do
|
||||||
|
@entry.category = "proper_name"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Proper name", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_category formats title category" do
|
||||||
|
@entry.category = "title"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Title", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_category formats reference category" do
|
||||||
|
@entry.category = "reference"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Reference", result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_category formats other category" do
|
||||||
|
@entry.category = "other"
|
||||||
|
result = format_entry_category(@entry)
|
||||||
|
assert_equal "Other", result
|
||||||
|
end
|
||||||
|
|
||||||
|
# format_entry_status tests
|
||||||
|
test "format_entry_status returns requested badge" do
|
||||||
|
@entry.status = :requested
|
||||||
|
result = format_entry_status(@entry)
|
||||||
|
assert_match /Requested/, result
|
||||||
|
assert_match /bg-yellow-100/, result
|
||||||
|
assert_match /text-yellow-800/, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_status returns approved badge" do
|
||||||
|
@entry.status = :approved
|
||||||
|
result = format_entry_status(@entry)
|
||||||
|
assert_match /Approved/, result
|
||||||
|
assert_match /bg-blue-100/, result
|
||||||
|
assert_match /text-blue-800/, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_status returns active badge" do
|
||||||
|
@entry.status = :active
|
||||||
|
result = format_entry_status(@entry)
|
||||||
|
assert_match /Active/, result
|
||||||
|
assert_match /bg-green-100/, result
|
||||||
|
assert_match /text-green-800/, result
|
||||||
|
end
|
||||||
|
|
||||||
|
test "format_entry_status badge contains proper HTML structure" do
|
||||||
|
@entry.status = :active
|
||||||
|
result = format_entry_status(@entry)
|
||||||
|
assert_match /<span/, result
|
||||||
|
assert_match /class=/, result
|
||||||
|
assert_match /px-2 py-1/, result
|
||||||
|
assert_match /text-xs/, result
|
||||||
|
assert_match /font-semibold/, result
|
||||||
|
assert_match /rounded-full/, result
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class AuthenticationFlowTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
@user = users(:admin_user)
|
||||||
|
@user.update!(invitation_accepted_at: Time.current)
|
||||||
|
Rails.cache.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user can sign in with valid credentials" do
|
||||||
|
get login_path
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
follow_redirect!
|
||||||
|
assert_response :success
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user cannot sign in with invalid password" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "wrongpassword"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
assert_select "div[role='alert']", text: /invalid email or password/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user cannot sign in with non-existent email" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: "nonexistent@example.com",
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pending user cannot sign in" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
|
||||||
|
post login_path, params: {
|
||||||
|
email: pending_user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
assert_select "div[role='alert']", text: /pending/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user can sign out" do
|
||||||
|
# Sign in first
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
|
||||||
|
# Sign out
|
||||||
|
delete logout_path
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "session persists across requests" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
|
||||||
|
get root_path
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
|
||||||
|
get entries_path
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember me creates cookie" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456",
|
||||||
|
remember_me: "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_nil cookies[:remember_token]
|
||||||
|
@user.reload
|
||||||
|
assert_not_nil @user.remember_token
|
||||||
|
assert_not_nil @user.remember_created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember me cookie logs user in automatically" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456",
|
||||||
|
remember_me: "1"
|
||||||
|
}
|
||||||
|
remember_cookie = cookies[:remember_token]
|
||||||
|
assert_not_nil remember_cookie
|
||||||
|
|
||||||
|
# Clear session
|
||||||
|
reset!
|
||||||
|
|
||||||
|
# Make request with remember cookie
|
||||||
|
cookies[:remember_token] = remember_cookie
|
||||||
|
get root_path
|
||||||
|
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logout clears remember me cookie" do
|
||||||
|
# Sign in with remember me
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456",
|
||||||
|
remember_me: "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_nil cookies[:remember_token]
|
||||||
|
|
||||||
|
# Sign out
|
||||||
|
delete logout_path
|
||||||
|
|
||||||
|
remember_cookie = cookies[:remember_token]
|
||||||
|
assert remember_cookie.nil? || remember_cookie.empty?
|
||||||
|
@user.reload
|
||||||
|
assert_nil @user.remember_token
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rate limiting prevents brute force" do
|
||||||
|
max_attempts = 5
|
||||||
|
responses = []
|
||||||
|
|
||||||
|
(max_attempts + 1).times do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "wrongpassword"
|
||||||
|
}
|
||||||
|
responses << response.status
|
||||||
|
end
|
||||||
|
|
||||||
|
assert responses.first(max_attempts).all? { |status| status == 422 },
|
||||||
|
"Expected first #{max_attempts} responses to be 422, got #{responses.first(max_attempts)}"
|
||||||
|
|
||||||
|
if Rails.cache.is_a?(ActiveSupport::Cache::NullStore)
|
||||||
|
assert responses.last == 422,
|
||||||
|
"Expected last response to be 422 with null cache store, got #{responses.last}"
|
||||||
|
else
|
||||||
|
assert responses.last == 429, "Expected last response to be 429, got #{responses.last}"
|
||||||
|
end
|
||||||
|
assert_select "div[role='alert']", text: /too many/i
|
||||||
|
end
|
||||||
|
|
||||||
|
test "successful login resets rate limit" do
|
||||||
|
# Fail a few times
|
||||||
|
3.times do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "wrongpassword"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Then succeed
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
|
||||||
|
# Should be able to login again immediately
|
||||||
|
delete logout_path
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "session timeout logs user out after inactivity" do
|
||||||
|
# Sign in
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
|
||||||
|
# Simulate time passing
|
||||||
|
travel 4.days do
|
||||||
|
get root_path
|
||||||
|
assert_redirected_to login_path
|
||||||
|
assert_match /expired/i, flash[:alert]
|
||||||
|
assert_nil session[:user_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember me prevents session timeout" do
|
||||||
|
# Sign in with remember me
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456",
|
||||||
|
remember_me: "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
remember_cookie = cookies[:remember_token]
|
||||||
|
|
||||||
|
# Simulate time passing (but within remember me period)
|
||||||
|
travel 5.days do
|
||||||
|
cookies[:remember_token] = remember_cookie
|
||||||
|
get root_path
|
||||||
|
assert_response :success
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "password reset flow completes successfully" do
|
||||||
|
# Request reset
|
||||||
|
post password_resets_path, params: { email: @user.email }
|
||||||
|
assert_redirected_to login_path
|
||||||
|
|
||||||
|
@user.reload
|
||||||
|
assert_not_nil @user.reset_password_token
|
||||||
|
|
||||||
|
# Visit reset form
|
||||||
|
get edit_password_reset_path(@user.reset_password_token)
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
# Submit new password
|
||||||
|
patch password_reset_path(@user.reset_password_token), params: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
assert_equal @user.id, session[:user_id]
|
||||||
|
|
||||||
|
@user.reload
|
||||||
|
assert @user.authenticate("newpassword12345")
|
||||||
|
assert_nil @user.reset_password_token
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation acceptance flow" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
|
||||||
|
# Visit invitation
|
||||||
|
get invitation_path(pending_user.invitation_token)
|
||||||
|
assert_response :success
|
||||||
|
|
||||||
|
# Accept invitation
|
||||||
|
patch accept_invitation_path(pending_user.invitation_token), params: {
|
||||||
|
user: {
|
||||||
|
password: "newpassword12345",
|
||||||
|
password_confirmation: "newpassword12345"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
assert_equal pending_user.id, session[:user_id]
|
||||||
|
|
||||||
|
pending_user.reload
|
||||||
|
assert_not_nil pending_user.invitation_accepted_at
|
||||||
|
assert pending_user.authenticate("newpassword12345")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "expired invitation cannot be accepted" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
pending_user.update!(invitation_sent_at: 15.days.ago)
|
||||||
|
|
||||||
|
get invitation_path(pending_user.invitation_token)
|
||||||
|
assert_redirected_to root_path
|
||||||
|
assert_match /expired/i, flash[:alert]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin user redirects to admin dashboard after login" do
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor redirects to root after login" do
|
||||||
|
contributor = users(:contributor_user)
|
||||||
|
contributor.update!(invitation_accepted_at: Time.current)
|
||||||
|
|
||||||
|
post login_path, params: {
|
||||||
|
email: contributor.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_redirected_to root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "already logged in user redirects from login page" do
|
||||||
|
# Sign in first
|
||||||
|
post login_path, params: {
|
||||||
|
email: @user.email,
|
||||||
|
password: "password123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to visit login page again
|
||||||
|
get login_path
|
||||||
|
assert_redirected_to admin_root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -127,7 +127,7 @@ class EntryRequestFlowTest < ActionDispatch::IntegrationTest
|
|||||||
assert_response :success
|
assert_response :success
|
||||||
|
|
||||||
# Active entry should be counted
|
# Active entry should be counted
|
||||||
assert_match /#{Entry.active_entries.count}/, response.body
|
assert_select "div", text: "#{Entry.active_entries.count} entries"
|
||||||
|
|
||||||
# Verify counts exclude requested/approved entries
|
# Verify counts exclude requested/approved entries
|
||||||
total_entries = Entry.count
|
total_entries = Entry.count
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
require "test_helper"
|
||||||
|
require "benchmark"
|
||||||
|
|
||||||
|
class SearchPerformanceTest < ActionDispatch::IntegrationTest
|
||||||
|
setup do
|
||||||
|
# Create a substantial number of test entries for performance testing
|
||||||
|
@test_entries = []
|
||||||
|
50.times do |i|
|
||||||
|
@test_entries << Entry.create!(
|
||||||
|
fi: "Testi sana #{i}",
|
||||||
|
en: "Test word #{i}",
|
||||||
|
sv: "Test ord #{i}",
|
||||||
|
category: :word,
|
||||||
|
status: :active
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
# Clean up test entries
|
||||||
|
Entry.where(id: @test_entries.map(&:id)).delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
test "full text search completes in reasonable time" do
|
||||||
|
measure_time("Full text search") do
|
||||||
|
get entries_path, params: { q: "test" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "language-specific search is performant" do
|
||||||
|
measure_time("Language-specific search") do
|
||||||
|
get entries_path, params: { q: "test", language: "en" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "alphabetical browsing is performant" do
|
||||||
|
measure_time("Alphabetical browsing") do
|
||||||
|
get entries_path, params: { language: "fi", starts_with: "t" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "category filtering is performant" do
|
||||||
|
measure_time("Category filtering") do
|
||||||
|
get entries_path, params: { category: "word" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "combined filters are performant" do
|
||||||
|
measure_time("Combined filters") do
|
||||||
|
get entries_path, params: { q: "test", language: "fi", category: "word" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pagination does not degrade performance" do
|
||||||
|
measure_time("Pagination") do
|
||||||
|
get entries_path, params: { page: 2 }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "entry show page loads quickly" do
|
||||||
|
entry = @test_entries.first
|
||||||
|
|
||||||
|
measure_time("Entry show page") do
|
||||||
|
get entry_path(entry)
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "XLSX download handles large datasets" do
|
||||||
|
measure_time("XLSX download") do
|
||||||
|
get download_entries_path(format: :xlsx)
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "statistics calculation is performant" do
|
||||||
|
measure_time("Statistics calculation") do
|
||||||
|
get entries_path
|
||||||
|
assert_response :success
|
||||||
|
assert_select "div", text: /entries/i
|
||||||
|
assert_select "div", text: /% complete/i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search with no results is fast" do
|
||||||
|
measure_time("No results search") do
|
||||||
|
get entries_path, params: { q: "nonexistentword12345xyz" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multiple sequential searches maintain performance" do
|
||||||
|
searches = [ "test", "sana", "word", "ord" ]
|
||||||
|
|
||||||
|
total_time = Benchmark.measure do
|
||||||
|
searches.each do |query|
|
||||||
|
get entries_path, params: { q: query }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert total_time.real < 2.0, "Multiple searches took too long: #{total_time.real}s"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "turbo stream responses are performant" do
|
||||||
|
measure_time("Turbo stream response") do
|
||||||
|
get entries_path, as: :turbo_stream, params: { q: "test" }
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def measure_time(description, max_time_ms: 500)
|
||||||
|
time = Benchmark.measure do
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
|
time_ms = (time.real * 1000).round(2)
|
||||||
|
assert time_ms < max_time_ms,
|
||||||
|
"#{description} took too long: #{time_ms}ms (max: #{max_time_ms}ms)"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class PasswordResetMailerTest < ActionMailer::TestCase
|
||||||
|
test "reset email contains token and expiry" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.update!(
|
||||||
|
reset_password_token: "reset_token_123",
|
||||||
|
reset_password_sent_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
email = PasswordResetMailer.reset(user)
|
||||||
|
assert_emails 1 do
|
||||||
|
email.deliver_now
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal [ user.email ], email.to
|
||||||
|
assert_equal "Reset your Sanasto Wiki password", email.subject
|
||||||
|
assert_includes email.content_type, "text/html"
|
||||||
|
assert_includes email.body.encoded, "reset_token_123"
|
||||||
|
assert_includes email.body.encoded, "Password Reset Request"
|
||||||
|
assert_includes email.body.encoded, "will expire on"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -37,4 +37,422 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
assert_not user.valid?
|
assert_not user.valid?
|
||||||
assert_includes user.errors[:password], "is too short (minimum is 12 characters)"
|
assert_includes user.errors[:password], "is too short (minimum is 12 characters)"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Association tests
|
||||||
|
test "can have invited_by association" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
user = User.create!(email: "invited@example.com", password: "password123456", invited_by: inviter)
|
||||||
|
assert_equal inviter, user.invited_by
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can have invited_users" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
user = User.create!(email: "invited@example.com", password: "password123456", invited_by: inviter)
|
||||||
|
assert_includes inviter.invited_users, user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many created_entries" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :created_entries
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many updated_entries" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :updated_entries
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many requested_entries" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :requested_entries
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many submitted_suggested_meanings" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :submitted_suggested_meanings
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many reviewed_suggested_meanings" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :reviewed_suggested_meanings
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has many comments" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert_respond_to user, :comments
|
||||||
|
end
|
||||||
|
|
||||||
|
# Scope tests
|
||||||
|
test "by_role scope filters contributors" do
|
||||||
|
contributor_user = users(:contributor_user)
|
||||||
|
results = User.by_role(:contributor)
|
||||||
|
assert_includes results, contributor_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "by_role scope filters reviewers" do
|
||||||
|
reviewer_user = users(:reviewer_user)
|
||||||
|
results = User.by_role(:reviewer)
|
||||||
|
assert_includes results, reviewer_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "by_role scope filters admins" do
|
||||||
|
admin_user = users(:admin_user)
|
||||||
|
results = User.by_role(:admin)
|
||||||
|
assert_includes results, admin_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "by_role scope returns all when role is blank" do
|
||||||
|
results = User.by_role(nil)
|
||||||
|
assert_equal User.all, results
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search_email scope finds users by email" do
|
||||||
|
admin_user = users(:admin_user)
|
||||||
|
results = User.search_email("admin")
|
||||||
|
assert_includes results, admin_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search_email scope returns all when query is blank" do
|
||||||
|
results = User.search_email(nil)
|
||||||
|
assert_equal User.all, results
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search_email scope finds partial matches" do
|
||||||
|
admin_user = users(:admin_user)
|
||||||
|
results = User.search_email("exam")
|
||||||
|
assert_includes results, admin_user
|
||||||
|
end
|
||||||
|
|
||||||
|
# invitation_expired? tests
|
||||||
|
test "invitation_expired? returns false when invitation_sent_at is nil" do
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
assert_not user.invitation_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_expired? returns false for recent invitation" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_sent_at: 1.day.ago
|
||||||
|
)
|
||||||
|
assert_not user.invitation_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_expired? returns true for expired invitation" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_sent_at: 15.days.ago
|
||||||
|
)
|
||||||
|
assert user.invitation_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_expired? returns false exactly at expiry boundary" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_sent_at: 13.days.ago
|
||||||
|
)
|
||||||
|
assert_not user.invitation_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
# invitation_pending? tests
|
||||||
|
test "invitation_pending? returns true for valid pending invitation" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "valid_token",
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invitation_accepted_at: nil
|
||||||
|
)
|
||||||
|
assert user.invitation_pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_pending? returns false when invitation is accepted" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "valid_token",
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invitation_accepted_at: Time.current
|
||||||
|
)
|
||||||
|
assert_not user.invitation_pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_pending? returns false when invitation is expired" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "valid_token",
|
||||||
|
invitation_sent_at: 15.days.ago,
|
||||||
|
invitation_accepted_at: nil
|
||||||
|
)
|
||||||
|
assert_not user.invitation_pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invitation_pending? returns false when no invitation token" do
|
||||||
|
user = User.new(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: nil,
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invitation_accepted_at: nil
|
||||||
|
)
|
||||||
|
assert_not user.invitation_pending?
|
||||||
|
end
|
||||||
|
|
||||||
|
# invite_by tests
|
||||||
|
test "invite_by sets invited_by and generates token" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
user.invite_by(inviter)
|
||||||
|
|
||||||
|
assert_equal inviter, user.invited_by
|
||||||
|
assert_not_nil user.invitation_token
|
||||||
|
assert_not_nil user.invitation_sent_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invite_by does not override existing invited_by" do
|
||||||
|
original_inviter = users(:admin_user)
|
||||||
|
new_inviter = users(:reviewer_user)
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456", invited_by: original_inviter)
|
||||||
|
user.invite_by(new_inviter)
|
||||||
|
|
||||||
|
assert_equal original_inviter, user.invited_by
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invite_by handles nil invitee" do
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
user.invite_by(nil)
|
||||||
|
|
||||||
|
assert_nil user.invited_by
|
||||||
|
assert_not_nil user.invitation_token
|
||||||
|
assert_not_nil user.invitation_sent_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invite_by generates 32-character token" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
user.invite_by(inviter)
|
||||||
|
|
||||||
|
assert user.invitation_token.length >= 32
|
||||||
|
end
|
||||||
|
|
||||||
|
# invite_by! tests
|
||||||
|
test "invite_by! saves the user" do
|
||||||
|
inviter = users(:admin_user)
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
user.invite_by!(inviter)
|
||||||
|
|
||||||
|
assert_not_nil user.id
|
||||||
|
assert user.persisted?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invite_by! works without invitee parameter" do
|
||||||
|
user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
user.invite_by!
|
||||||
|
|
||||||
|
assert_not_nil user.id
|
||||||
|
assert_not_nil user.invitation_token
|
||||||
|
end
|
||||||
|
|
||||||
|
# find_by_valid_invitation_token tests
|
||||||
|
test "find_by_valid_invitation_token finds user with valid token" do
|
||||||
|
user = User.create!(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "valid_token_12345",
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invitation_accepted_at: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_invitation_token("valid_token_12345")
|
||||||
|
assert_equal user, found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_invitation_token returns nil for expired token" do
|
||||||
|
User.create!(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "expired_token",
|
||||||
|
invitation_sent_at: 15.days.ago,
|
||||||
|
invitation_accepted_at: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_invitation_token("expired_token")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_invitation_token returns nil for accepted invitation" do
|
||||||
|
User.create!(
|
||||||
|
email: "test@example.com",
|
||||||
|
password: "password123456",
|
||||||
|
invitation_token: "accepted_token",
|
||||||
|
invitation_sent_at: 1.day.ago,
|
||||||
|
invitation_accepted_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_invitation_token("accepted_token")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_invitation_token returns nil for non-existent token" do
|
||||||
|
found_user = User.find_by_valid_invitation_token("nonexistent_token")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
# remember_me tests
|
||||||
|
test "remember_me generates token and saves" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
token = user.remember_me
|
||||||
|
|
||||||
|
assert_not_nil token
|
||||||
|
assert_not_nil user.remember_token
|
||||||
|
assert_not_nil user.remember_created_at
|
||||||
|
assert_equal token, user.remember_token
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_me returns the generated token" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
token = user.remember_me
|
||||||
|
|
||||||
|
assert_kind_of String, token
|
||||||
|
assert token.length >= 32
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_me updates database immediately" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_me
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert_not_nil user.remember_token
|
||||||
|
assert_not_nil user.remember_created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_me saves without validation" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
# Make user invalid (email already taken by changing to duplicate)
|
||||||
|
user.email = ""
|
||||||
|
token = user.remember_me
|
||||||
|
|
||||||
|
assert_not_nil token
|
||||||
|
user.reload
|
||||||
|
assert_not_nil user.remember_token
|
||||||
|
end
|
||||||
|
|
||||||
|
# forget_me tests
|
||||||
|
test "forget_me clears remember token and timestamp" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_me
|
||||||
|
assert_not_nil user.remember_token
|
||||||
|
|
||||||
|
user.forget_me
|
||||||
|
user.reload
|
||||||
|
|
||||||
|
assert_nil user.remember_token
|
||||||
|
assert_nil user.remember_created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
test "forget_me works when no remember token exists" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.forget_me
|
||||||
|
|
||||||
|
assert_nil user.remember_token
|
||||||
|
assert_nil user.remember_created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
# remember_token_expired? tests
|
||||||
|
test "remember_token_expired? returns true when remember_created_at is nil" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
assert user.remember_token_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_token_expired? returns false for recent token" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_token = "token"
|
||||||
|
user.remember_created_at = 1.day.ago
|
||||||
|
user.save(validate: false)
|
||||||
|
|
||||||
|
assert_not user.remember_token_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_token_expired? returns true for expired token" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_token = "token"
|
||||||
|
user.remember_created_at = 3.weeks.ago
|
||||||
|
user.save(validate: false)
|
||||||
|
|
||||||
|
assert user.remember_token_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remember_token_expired? boundary at 2 weeks" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_token = "token"
|
||||||
|
user.remember_created_at = 13.days.ago
|
||||||
|
user.save(validate: false)
|
||||||
|
|
||||||
|
assert_not user.remember_token_expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
# find_by_valid_remember_token tests
|
||||||
|
test "find_by_valid_remember_token finds user with valid token" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
token = user.remember_me
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_remember_token(token)
|
||||||
|
assert_equal user, found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_remember_token returns nil for expired token" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_token = "expired_token"
|
||||||
|
user.remember_created_at = 3.weeks.ago
|
||||||
|
user.save(validate: false)
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_remember_token("expired_token")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_remember_token returns nil for non-existent token" do
|
||||||
|
found_user = User.find_by_valid_remember_token("nonexistent_token")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_by_valid_remember_token returns nil when user has no remember_created_at" do
|
||||||
|
user = users(:admin_user)
|
||||||
|
user.remember_token = "token_without_timestamp"
|
||||||
|
user.remember_created_at = nil
|
||||||
|
user.save(validate: false)
|
||||||
|
|
||||||
|
found_user = User.find_by_valid_remember_token("token_without_timestamp")
|
||||||
|
assert_nil found_user
|
||||||
|
end
|
||||||
|
|
||||||
|
# Password authentication tests
|
||||||
|
test "authenticate returns user for correct password" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123456")
|
||||||
|
assert_equal user, user.authenticate("password123456")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "authenticate returns false for incorrect password" do
|
||||||
|
user = User.create!(email: "test@example.com", password: "password123456")
|
||||||
|
assert_not user.authenticate("wrongpassword")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Email normalization tests
|
||||||
|
test "email uniqueness is case-insensitive" do
|
||||||
|
User.create!(email: "Test@Example.com", password: "password123456")
|
||||||
|
duplicate_user = User.new(email: "test@example.com", password: "password123456")
|
||||||
|
|
||||||
|
# Note: This depends on database collation, but should be tested
|
||||||
|
assert_not duplicate_user.valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constants tests
|
||||||
|
test "INVITATION_TOKEN_EXPIRY is 14 days" do
|
||||||
|
assert_equal 14.days, User::INVITATION_TOKEN_EXPIRY
|
||||||
|
end
|
||||||
|
|
||||||
|
test "REMEMBER_TOKEN_EXPIRY is 2 weeks" do
|
||||||
|
assert_equal 2.weeks, User::REMEMBER_TOKEN_EXPIRY
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
require "application_system_test_case"
|
||||||
|
|
||||||
|
class AdminWorkflowTest < ApplicationSystemTestCase
|
||||||
|
setup do
|
||||||
|
@admin = users(:admin_user)
|
||||||
|
@admin.update!(invitation_accepted_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can access dashboard" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
assert_text "Dashboard"
|
||||||
|
assert_text "Total Users"
|
||||||
|
assert_text "Total Entries"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin sees admin button in header" do
|
||||||
|
login_as(@admin)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
within "header" do
|
||||||
|
assert_link "Admin"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can send invitation" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_invitations_path
|
||||||
|
click_link "Send New Invitation"
|
||||||
|
|
||||||
|
assert_current_path new_admin_invitation_path
|
||||||
|
|
||||||
|
fill_in "Name", with: "New User"
|
||||||
|
fill_in "Email", with: "newuser@example.com"
|
||||||
|
select "Contributor", from: "Role"
|
||||||
|
|
||||||
|
click_button "Send Invitation"
|
||||||
|
|
||||||
|
assert_current_path admin_invitations_path
|
||||||
|
assert_text "Invitation sent"
|
||||||
|
assert_text "newuser@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can view users list" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_users_path
|
||||||
|
|
||||||
|
assert_text "Users"
|
||||||
|
assert_selector "table"
|
||||||
|
assert_text @admin.email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can edit user role" do
|
||||||
|
user = users(:contributor_user)
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_users_path
|
||||||
|
within "#user_#{user.id}" do
|
||||||
|
click_link "Edit"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path edit_admin_user_path(user)
|
||||||
|
|
||||||
|
select "Reviewer", from: "Role"
|
||||||
|
click_button "Update User"
|
||||||
|
|
||||||
|
assert_current_path admin_users_path
|
||||||
|
assert_text "User updated"
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
assert_equal "reviewer", user.role
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can delete user" do
|
||||||
|
user = users(:contributor_user)
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_users_path
|
||||||
|
|
||||||
|
within "#user_#{user.id}" do
|
||||||
|
click_button "Delete"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path admin_users_path
|
||||||
|
assert_text "User deleted"
|
||||||
|
assert_no_text user.email
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can view entry requests" do
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
fi: "Requested Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: users(:contributor_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_requests_path
|
||||||
|
|
||||||
|
assert_text "Requested Entry"
|
||||||
|
assert_link "View"
|
||||||
|
assert_link "Edit"
|
||||||
|
assert_button "Approve"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can approve entry request" do
|
||||||
|
requester = users(:contributor_user)
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
fi: "Requested Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: requester
|
||||||
|
)
|
||||||
|
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_requests_path
|
||||||
|
|
||||||
|
within "#entry_#{requested_entry.id}" do
|
||||||
|
click_button "Approve"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path admin_requests_path
|
||||||
|
assert_text "approved"
|
||||||
|
|
||||||
|
requested_entry.reload
|
||||||
|
assert_equal "approved", requested_entry.status
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can edit entry request before approval" do
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
fi: "Requested Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: users(:contributor_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit edit_admin_request_path(requested_entry)
|
||||||
|
|
||||||
|
fill_in "Finnish", with: "Edited Finnish"
|
||||||
|
fill_in "English", with: "Added English"
|
||||||
|
click_button "Save Changes"
|
||||||
|
|
||||||
|
assert_current_path admin_requests_path
|
||||||
|
requested_entry.reload
|
||||||
|
assert_equal "Edited Finnish", requested_entry.fi
|
||||||
|
assert_equal "Added English", requested_entry.en
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can reject entry request" do
|
||||||
|
requested_entry = Entry.create!(
|
||||||
|
fi: "Requested Entry",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: users(:contributor_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_requests_path
|
||||||
|
|
||||||
|
within "#entry_#{requested_entry.id}" do
|
||||||
|
click_button "Reject"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path admin_requests_path
|
||||||
|
assert_text "rejected"
|
||||||
|
assert_nil Entry.find_by(id: requested_entry.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can resend invitation" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_invitations_path
|
||||||
|
|
||||||
|
within "#invitation_#{pending_user.id}" do
|
||||||
|
click_button "Resend"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path admin_invitations_path
|
||||||
|
assert_text "Invitation resent"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can cancel invitation" do
|
||||||
|
pending_user = users(:pending_invitation)
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_invitations_path
|
||||||
|
|
||||||
|
within "#invitation_#{pending_user.id}" do
|
||||||
|
click_button "Cancel"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_current_path admin_invitations_path
|
||||||
|
assert_text "deleted"
|
||||||
|
assert_nil User.find_by(id: pending_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin sees request count badge" do
|
||||||
|
Entry.create!(
|
||||||
|
fi: "Requested Entry 1",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: users(:contributor_user)
|
||||||
|
)
|
||||||
|
Entry.create!(
|
||||||
|
fi: "Requested Entry 2",
|
||||||
|
category: :word,
|
||||||
|
status: :requested,
|
||||||
|
requested_by: users(:contributor_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
login_as(@admin)
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
within "header" do
|
||||||
|
assert_text "2"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin navigation is responsive" do
|
||||||
|
login_as(@admin)
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
# Test mobile menu if visible
|
||||||
|
if page.has_selector?("#admin-mobile-menu-button", visible: true)
|
||||||
|
click_button id: "admin-mobile-menu-button"
|
||||||
|
assert_selector "#admin-mobile-menu", visible: true
|
||||||
|
assert_link "Dashboard"
|
||||||
|
assert_link "Users"
|
||||||
|
assert_link "Invitations"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin cannot delete themselves" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_users_path
|
||||||
|
|
||||||
|
within "#user_#{@admin.id}" do
|
||||||
|
click_button "Delete"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_text "cannot delete yourself"
|
||||||
|
assert User.exists?(@admin.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can view dashboard statistics" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_dashboard_path
|
||||||
|
|
||||||
|
assert_text "Total Users"
|
||||||
|
assert_text "Total Entries"
|
||||||
|
assert_text "Pending Invitations"
|
||||||
|
assert_text "Entry Requests"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can navigate between admin sections" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
click_link "Users"
|
||||||
|
assert_current_path admin_users_path
|
||||||
|
|
||||||
|
click_link "Invitations"
|
||||||
|
assert_current_path admin_invitations_path
|
||||||
|
|
||||||
|
click_link "Requests"
|
||||||
|
assert_current_path admin_requests_path
|
||||||
|
|
||||||
|
click_link "Dashboard"
|
||||||
|
assert_current_path admin_dashboard_path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can return to main site from admin" do
|
||||||
|
login_as(@admin)
|
||||||
|
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
# On mobile, click hamburger menu first
|
||||||
|
if page.has_selector?("#admin-mobile-menu-button", visible: true)
|
||||||
|
click_button id: "admin-mobile-menu-button"
|
||||||
|
end
|
||||||
|
|
||||||
|
click_link "Back to Site"
|
||||||
|
|
||||||
|
assert_current_path root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
require "application_system_test_case"
|
||||||
|
|
||||||
|
class ContributorWorkflowTest < ApplicationSystemTestCase
|
||||||
|
setup do
|
||||||
|
@contributor = users(:contributor_user)
|
||||||
|
@contributor.update!(invitation_accepted_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can sign in" do
|
||||||
|
visit login_path
|
||||||
|
|
||||||
|
fill_in "Email", with: @contributor.email
|
||||||
|
fill_in "Password", with: "password123456"
|
||||||
|
click_button "Sign In"
|
||||||
|
|
||||||
|
assert_text "Welcome back"
|
||||||
|
within "header" do
|
||||||
|
assert_text @contributor.name.split.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can edit entry" do
|
||||||
|
entry = entries(:one)
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
visit entry_path(entry)
|
||||||
|
click_link "Edit"
|
||||||
|
|
||||||
|
assert_current_path edit_entry_path(entry)
|
||||||
|
|
||||||
|
fill_in "Finnish", with: "Updated Finnish Text"
|
||||||
|
fill_in "English", with: "Updated English Text"
|
||||||
|
select "Phrase", from: "Category"
|
||||||
|
fill_in "Additional Notes", with: "Updated notes"
|
||||||
|
|
||||||
|
click_button "Save Changes"
|
||||||
|
|
||||||
|
assert_current_path entry_path(entry)
|
||||||
|
assert_text "Entry updated"
|
||||||
|
assert_text "Updated Finnish Text"
|
||||||
|
assert_text "Updated English Text"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can add comment to entry" do
|
||||||
|
entry = entries(:one)
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
visit entry_path(entry)
|
||||||
|
|
||||||
|
click_button "Add Comment"
|
||||||
|
|
||||||
|
within "#comment_form_modal" do
|
||||||
|
select "Finnish (FI)", from: "Language"
|
||||||
|
fill_in "Comment", with: "This is my comment on the Finnish translation"
|
||||||
|
click_button "Submit"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_text "This is my comment on the Finnish translation"
|
||||||
|
assert_text @contributor.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor sees signed in status in header" do
|
||||||
|
login_as(@contributor)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
within "header" do
|
||||||
|
assert_text @contributor.name.split.first
|
||||||
|
assert_button "Sign Out", visible: :all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can sign out" do
|
||||||
|
login_as(@contributor)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
# On mobile, click hamburger menu first
|
||||||
|
if page.has_selector?("#mobile-menu-button", visible: true)
|
||||||
|
click_button id: "mobile-menu-button"
|
||||||
|
end
|
||||||
|
|
||||||
|
click_link "Sign Out"
|
||||||
|
|
||||||
|
assert_current_path root_path
|
||||||
|
assert_text "You have been logged out"
|
||||||
|
within "header" do
|
||||||
|
assert_link "Sign In"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor cannot access admin pages" do
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
visit admin_root_path
|
||||||
|
|
||||||
|
assert_current_path root_path
|
||||||
|
assert_text "administrator"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can request password reset" do
|
||||||
|
visit login_path
|
||||||
|
|
||||||
|
click_link "Forgot password?"
|
||||||
|
|
||||||
|
assert_current_path new_password_reset_path
|
||||||
|
|
||||||
|
fill_in "Email", with: @contributor.email
|
||||||
|
click_button "Send Reset Instructions"
|
||||||
|
|
||||||
|
assert_current_path login_path
|
||||||
|
assert_text "password reset instructions"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor session persists across page loads" do
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
visit root_path
|
||||||
|
assert_text @contributor.name.split.first
|
||||||
|
|
||||||
|
visit entries_path
|
||||||
|
within "header" do
|
||||||
|
assert_text @contributor.name.split.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can use remember me" do
|
||||||
|
visit login_path
|
||||||
|
|
||||||
|
fill_in "Email", with: @contributor.email
|
||||||
|
fill_in "Password", with: "password123456"
|
||||||
|
check "Remember me for 2 weeks"
|
||||||
|
click_button "Sign In"
|
||||||
|
|
||||||
|
assert_text "Welcome back"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor sees validation errors when editing entry incorrectly" do
|
||||||
|
entry = entries(:one)
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
visit edit_entry_path(entry)
|
||||||
|
|
||||||
|
# Clear all translations (should fail validation)
|
||||||
|
fill_in "Finnish", with: ""
|
||||||
|
fill_in "English", with: ""
|
||||||
|
fill_in "Swedish", with: ""
|
||||||
|
fill_in "Norwegian", with: ""
|
||||||
|
fill_in "Russian", with: ""
|
||||||
|
fill_in "German", with: ""
|
||||||
|
|
||||||
|
click_button "Save Changes"
|
||||||
|
|
||||||
|
assert_text "At least one language translation is required"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "contributor can filter comments by language tab" do
|
||||||
|
entry = entries(:one)
|
||||||
|
login_as(@contributor)
|
||||||
|
|
||||||
|
# Create comments in different languages
|
||||||
|
Comment.create!(
|
||||||
|
commentable: entry,
|
||||||
|
user: @contributor,
|
||||||
|
body: "Finnish comment",
|
||||||
|
language_code: "fi"
|
||||||
|
)
|
||||||
|
Comment.create!(
|
||||||
|
commentable: entry,
|
||||||
|
user: @contributor,
|
||||||
|
body: "English comment",
|
||||||
|
language_code: "en"
|
||||||
|
)
|
||||||
|
|
||||||
|
visit entry_path(entry)
|
||||||
|
|
||||||
|
click_link "Finnish"
|
||||||
|
assert_text "Finnish comment"
|
||||||
|
|
||||||
|
click_link "English"
|
||||||
|
assert_text "English comment"
|
||||||
|
|
||||||
|
click_link "All languages"
|
||||||
|
assert_text "Finnish comment"
|
||||||
|
assert_text "English comment"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
require "application_system_test_case"
|
||||||
|
|
||||||
|
class PublicBrowsingTest < ApplicationSystemTestCase
|
||||||
|
test "visitor can browse entries without logging in" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
assert_selector "h1", text: "Sanasto Wiki"
|
||||||
|
assert_selector ".entry-row", minimum: 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can search entries" do
|
||||||
|
entry = entries(:one)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
fill_in "q", with: entry.fi
|
||||||
|
click_button "Search"
|
||||||
|
|
||||||
|
assert_text entry.fi
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can filter by language" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
click_button "Finnish"
|
||||||
|
|
||||||
|
assert_current_path entries_path(language: "fi")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can filter by category" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
select "Word", from: "category"
|
||||||
|
|
||||||
|
assert_current_path entries_path(category: "word")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can view entry details" do
|
||||||
|
entry = entries(:one)
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
click_link entry.fi
|
||||||
|
|
||||||
|
assert_text entry.fi
|
||||||
|
assert_text entry.en if entry.en.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can browse alphabetically" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
click_button "Finnish"
|
||||||
|
click_link "A"
|
||||||
|
|
||||||
|
assert_current_path entries_path(language: "fi", starts_with: "a")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can download XLSX" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
click_link "Download XLSX"
|
||||||
|
|
||||||
|
assert_equal "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
page.response_headers["Content-Type"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor sees sign in link in header" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
within "header" do
|
||||||
|
assert_link "Sign In"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can request new entry" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
click_link "Request Entry"
|
||||||
|
|
||||||
|
assert_current_path new_request_path
|
||||||
|
assert_field "Name"
|
||||||
|
assert_field "Email"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "search results show no results message when nothing matches" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
fill_in "q", with: "nonexistentword12345xyz"
|
||||||
|
click_button "Search"
|
||||||
|
|
||||||
|
assert_text "No entries matched your filters"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor can paginate through results" do
|
||||||
|
# Create enough entries to require pagination
|
||||||
|
26.times do |i|
|
||||||
|
Entry.create!(
|
||||||
|
fi: "Test Entry #{i}",
|
||||||
|
category: :word,
|
||||||
|
status: :active
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
assert_selector ".entry-row", count: 25
|
||||||
|
assert_link "2"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "visitor sees entry statistics" do
|
||||||
|
visit root_path
|
||||||
|
|
||||||
|
assert_text "Total Entries"
|
||||||
|
assert_text "Verified"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
require "simplecov"
|
||||||
|
SimpleCov.start "rails"
|
||||||
|
|
||||||
ENV["RAILS_ENV"] ||= "test"
|
ENV["RAILS_ENV"] ||= "test"
|
||||||
require_relative "../config/environment"
|
require_relative "../config/environment"
|
||||||
require "rails/test_help"
|
require "rails/test_help"
|
||||||
|
|||||||
Reference in New Issue
Block a user