2023-09-14 13:37:09 +00:00
# typed: false
2018-03-21 20:36:49 +00:00
require " rails_helper "
2018-03-13 17:55:11 +00:00
describe Search do
# We need to set up and then teardown the environment
# outside of the typical RSpec transaction because
# the search module uses custom SQL that doesn't
# work inside the transaction
before ( :all ) do
2023-09-25 13:51:24 +00:00
@alice = create ( :user , username : " alice " )
@bob = create ( :user , username : " bob " )
2018-03-13 17:55:11 +00:00
2018-06-14 13:34:59 +00:00
@multi_tag = create ( :story , title : " multitag term1 t1 t2 " ,
2018-03-13 17:55:11 +00:00
url : " https://example.com/3 " ,
2023-09-25 13:51:24 +00:00
user_id : @alice . id ,
2018-03-13 17:55:11 +00:00
tags_a : [ " tag1 " , " tag2 " ] )
@stories = [
2018-06-14 13:34:59 +00:00
create ( :story , title : " unique " ,
2018-03-13 17:55:11 +00:00
url : " https://example.com/unique " ,
2023-09-25 13:51:24 +00:00
user_id : @bob . id ,
2018-03-13 17:55:11 +00:00
tags_a : [ " tag1 " ] ) ,
2018-06-14 13:34:59 +00:00
create ( :story , title : " term1 domain1 " ,
2018-03-13 17:55:11 +00:00
url : " https://example.com/1 " ,
2023-09-25 13:51:24 +00:00
user_id : @alice . id ,
2018-03-13 17:55:11 +00:00
tags_a : [ " tag1 " ] ) ,
2018-06-14 13:34:59 +00:00
create ( :story , title : " term1 t2 " ,
2018-03-13 17:55:11 +00:00
url : " https://example.com/2 " ,
2023-09-25 13:51:24 +00:00
user_id : @bob . id ,
2018-03-13 17:55:11 +00:00
tags_a : [ " tag2 " ] ) ,
@multi_tag ,
2018-06-14 13:34:59 +00:00
create ( :story , title : " term1 domain2 " ,
2018-03-13 17:55:11 +00:00
url : " https://lobste.rs/1 " ,
2023-09-25 13:51:24 +00:00
user_id : @alice . id ,
2018-03-13 17:55:11 +00:00
tags_a : [ " tag1 " ] )
]
2023-09-06 15:49:24 +00:00
@stories . each do | s |
StoryText . create id : s . id , title : s . title , description : s . description
end
2018-06-11 23:28:11 +00:00
@comments = [
create ( :comment , comment : " comment0 " ,
story_id : @multi_tag . id ,
2023-09-25 13:51:24 +00:00
user_id : @bob . id ) ,
2018-06-11 23:28:11 +00:00
create ( :comment , comment : " comment1 " ,
story_id : @stories [ 0 ] . id ,
2023-09-25 13:51:24 +00:00
user_id : @alice . id ) ,
2018-06-11 23:28:11 +00:00
create ( :comment , comment : " comment2 " ,
story_id : @stories [ 1 ] . id ,
2023-09-25 13:51:24 +00:00
user_id : @alice . id ) ,
2018-06-11 23:28:11 +00:00
create ( :comment , comment : " comment3 " ,
story_id : @stories [ 2 ] . id ,
2023-09-25 13:51:24 +00:00
user_id : @bob . id ) ,
2018-06-11 23:28:11 +00:00
create ( :comment , comment : " comment4 " ,
story_id : @stories [ 4 ] . id ,
2023-09-25 13:51:24 +00:00
user_id : @bob . id )
2018-06-11 23:28:11 +00:00
]
2018-03-13 17:55:11 +00:00
end
after ( :all ) do
2018-06-11 23:28:11 +00:00
@comments . each ( & :destroy! )
2018-10-10 13:37:09 +00:00
@stories . flat_map ( & :votes ) . each ( & :destroy! )
2018-03-21 20:19:48 +00:00
@stories . each ( & :destroy! )
2023-09-25 13:51:24 +00:00
@alice & . destroy!
@bob & . destroy!
2018-03-13 17:55:11 +00:00
end
2023-09-06 15:49:24 +00:00
it " returns nothing when initialized empty " do
search = Search . new ( { } , nil )
# test is a bit brittle by coupling to the way the caching couples to the perform! dispatcher,
# but add db-query-matchers gem if test gets flaky
expect ( search ) . to_not receive ( :perform_story_search! )
expect ( search ) . to_not receive ( :perform_comment_search! )
2018-03-13 17:55:11 +00:00
2023-09-06 15:49:24 +00:00
expect ( search . results . length ) . to eq ( 0 )
end
it " doesn't permit sql injection " do
%w[ ' " % \\ ' \\ " \\ \\ ' \\ \\ " ] . each do | esc |
[
# stories
{ what : " stories " , q : " term #{ esc } " } ,
{ what : " stories " , q : " \" term #{ esc } \" " } ,
{ what : " stories " , q : " domain:foo #{ esc } " } ,
2023-09-25 13:51:24 +00:00
{ what : " stories " , q : " submitter:alice{esc} " } ,
{ what : " stories " , q : " submitter:@bob{esc} " } ,
2023-09-14 04:14:59 +00:00
{ what : " stories " , q : " tag:foo #{ esc } " } ,
{ what : " stories " , q : " title:titl #{ esc } " } ,
{ what : " stories " , q : " title: \" multi #{ esc } titl \" " } ,
2023-09-06 15:49:24 +00:00
{ what : " stories " , q : " term #{ esc } " } ,
{ what : " stories " , q : " term " , order : " newest #{ esc } " } ,
{ what : " stories " , q : " term " , page : " 2 #{ esc } " } ,
{ what : " stories #{ esc } " , q : " term " } ,
2023-09-14 14:38:08 +00:00
{ what : " stories " , q : " term 'two apostrophes' " } ,
2023-10-01 22:56:39 +00:00
{ what : " stories " , q : " 'go-sqlite' " } ,
# some real attack attempts:
{ what : " stories " , q : " tag:formalmethods tag:testing'' ORDER BY 1-- BjzD " } ,
{ what : " stories " , q : " tag:formalmethods tag:testing'fcvzLp<' \" >UkDPPc " } ,
{ what : " stories " , q : " tag:formalmethods tag:testing') AND EXTRACTVALUE(4050,CONCAT(0x5c,0x7170787171,(SELECT (ELT(4050=4050,1))),0x71627a6b71)) AND ('pDUW'='pDUW " } ,
2023-09-06 15:49:24 +00:00
# comments
{ what : " comments " , q : " term #{ esc } " } ,
{ what : " comments " , q : " \" term #{ esc } \" " } ,
{ what : " comments " , q : " domain:foo #{ esc } " } ,
2023-09-25 13:51:24 +00:00
{ what : " comments " , q : " submitter:carol{esc} " } ,
{ what : " comments " , q : " submitter:@dave{esc} " } ,
{ what : " comments " , q : " tag:foo #{ esc } " } ,
{ what : " comments " , q : " title:titl #{ esc } " } ,
{ what : " comments " , q : " title: \" multi #{ esc } titl \" " } ,
2023-09-06 15:49:24 +00:00
{ what : " comments " , q : " term #{ esc } " } ,
{ what : " comments " , q : " term " , order : " newest #{ esc } " } ,
{ what : " comments " , q : " term " , page : " 2 #{ esc } " } ,
{ what : " comments #{ esc } " , q : " term " }
] . each do | params |
# implicit assertion that no error was thrown for invalid SQL
expect ( Search . new ( params , nil ) . results . length ) . to eq ( 0 )
end
end
end
2023-09-16 03:31:22 +00:00
# + is the boolean mode operator meaning 'required'
2023-09-16 12:38:24 +00:00
it " doesn't error on odd real searches with punctuation " do
[
{ q : " c++ " } ,
{ q : " sudo-rs " } ,
{ q : " pi-hole " } ,
{ q : " header X-Powered-By: Express " } ,
{ q : " snake_case " }
] . each do | params |
2023-09-25 13:51:24 +00:00
search = Search . new ( params , @alice )
2023-09-16 12:38:24 +00:00
expect ( search . results_count ) . to be_an_instance_of ( Integer )
end
2023-09-16 03:31:22 +00:00
end
2023-09-06 15:49:24 +00:00
it " can search titles for stories " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " unique " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " unique " )
end
2019-05-09 01:44:53 +00:00
it " can search for multitagged stories " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " multitag " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " multitag term1 t1 t2 " )
end
it " can search for stories by domain " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " term1 domain:lobste.rs " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " term1 domain2 " )
end
2023-09-25 13:51:24 +00:00
it " can search for stories by submitter " do
search = Search . new ( { q : " submitter:bob " , what : " stories " } , nil )
expect ( search . results . length ) . to eq ( 2 )
expect ( search . results . map ( & :title ) . sort ) . to eq ( [ " term1 t2 " , " unique " ] )
end
2018-03-13 17:55:11 +00:00
it " can search for stories by tag " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " term1 tag:tag1 " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 3 )
2023-09-06 15:49:24 +00:00
# It's easy to search tags in a way that Rails thinks satisfies the preload request for
# story.tags, causing stories to only have the searched-for tags
multi_tag_res = search . results . select { | res | res . id == @multi_tag . id }
2018-03-13 17:55:11 +00:00
expect ( multi_tag_res . length ) . to eq ( 1 )
2023-09-06 15:49:24 +00:00
expect ( multi_tag_res . first . tags . map ( & :tag ) . sort ) . to eq ( [ " tag1 " , " tag2 " ] )
2018-03-13 17:55:11 +00:00
end
it " should return only stories with both tags if multiple tags are present " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " term1 tag:tag1 tag:tag2 " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 1 )
end
it " can search for stories with only tags " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " tag:tag2 " , what : " stories " } , @alice )
2018-03-13 17:55:11 +00:00
expect ( search . results . length ) . to eq ( 2 )
end
2018-06-11 23:28:11 +00:00
2023-09-14 04:14:59 +00:00
it " can search for stories by title " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " title:unique " , what : " stories " } , @alice )
2023-09-14 04:14:59 +00:00
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " unique " )
end
it " can search for stories by title with multiple words " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : 'title:"term1 t2"' , what : " stories " } , @alice )
2023-09-14 04:14:59 +00:00
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " term1 t2 " )
end
2023-09-25 13:51:24 +00:00
it " can search for stories by url " do
search = Search . new ( { q : " term1 https://lobste.rs/1 " , what : " stories " } , @alice )
expect ( search . results . length ) . to eq ( 1 )
expect ( search . results . first . title ) . to eq ( " term1 domain2 " )
end
2018-06-11 23:28:11 +00:00
it " can search for comments " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " comment1 " , what : " comments " } , @alice )
2018-06-11 23:28:11 +00:00
expect ( search . results ) . to include ( @comments [ 1 ] )
end
2019-10-07 00:01:18 +00:00
2018-06-11 23:28:11 +00:00
it " can search for comments by tag " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " comment2 tag:tag1 " , what : " comments " } , @alice )
2018-06-11 23:28:11 +00:00
expect ( search . results ) . to include ( @comments [ 2 ] )
expect ( search . results ) . not_to include ( @comments [ 3 ] )
end
2019-10-07 00:01:18 +00:00
2018-06-11 23:28:11 +00:00
it " can search for comments with only tags " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " tag:tag1 " , what : " comments " } , @alice )
2018-06-11 23:28:11 +00:00
expect ( search . results ) . to include ( @comments [ 2 ] )
expect ( search . results ) . not_to include ( @comments [ 3 ] )
end
2019-10-07 00:01:18 +00:00
2018-06-11 23:28:11 +00:00
it " should only return comments matching all tags if multiple are present " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " tag:tag1 tag:tag2 " , what : " comments " } , @alice )
2018-06-11 23:28:11 +00:00
expect ( search . results ) . to eq ( [ @comments [ 0 ] ] )
end
it " should only return comments with stories in domain if domain present " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " domain:lobste.rs " , what : " comments " } , @alice )
2018-06-11 23:28:11 +00:00
expect ( search . results ) . to include ( @comments [ 4 ] )
expect ( search . results ) . not_to include ( @comments [ 3 ] )
end
2023-09-16 14:25:22 +00:00
it " can search for comments by url " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " comment4 https://lobste.rs/1 " , what : " comments " } , @alice )
2023-09-16 14:25:22 +00:00
expect ( search . results ) . to eq ( [ @comments [ 4 ] ] )
end
2023-09-14 04:14:59 +00:00
it " can search for comments by story title " do
2023-09-25 13:51:24 +00:00
search = Search . new ( { q : " comment4 title:domain2 " , what : " comments " } , @alice )
2023-09-14 04:14:59 +00:00
expect ( search . results ) . to eq ( [ @comments [ 4 ] ] )
end
2023-09-25 13:51:24 +00:00
it " can search for comments by story submitter " do
search = Search . new ( { q : " submitter:bob " , what : " comments " } , nil )
expect ( search . results . length ) . to eq ( 2 )
end
2023-09-25 15:02:07 +00:00
it " can search for comments by commenter " do
search = Search . new ( { q : " commenter:bob " , what : " comments " } , nil )
expect ( search . results . length ) . to eq ( 3 )
end
2023-12-18 02:22:47 +00:00
describe " # flatten_title " do
it " flattens multiword searches to single sql term " do
s = Search . new ( { } , nil )
expect ( s . flatten_title ( { quoted : [ { term : " cool " } , { term : " beans " } ] } ) ) . to eq ( " \" cool beans \" " )
end
it " doesn't permit sql injection " do
s = Search . new ( { } , nil )
expect ( s . flatten_title ( { term : " as'df " } ) ) . to eq ( " as \\ 'df " )
expect ( s . flatten_title ( { term : " hj \" kl " } ) ) . to eq ( " hj \\ \" kl " )
expect ( s . flatten_title ( { quoted : [ { term : " cat' " } , { term : " scare " } ] } ) ) . to eq ( " \" cat \\ ' scare \" " )
end
end
describe " # strip_operators " do
it " doesn't permit sql injection " do
s = Search . new ( { } , nil )
expect ( s . strip_operators ( " as'df " ) ) . to eq ( " as \\ 'df " )
expect ( s . strip_operators ( " hj \" kl " ) ) . to eq ( " hj kl " )
expect ( s . strip_operators ( " li%ke " ) ) . to eq ( " li ke " )
expect ( s . strip_operators ( " \" blah \" " ) ) . to eq ( " blah " )
end
end
2018-03-13 17:55:11 +00:00
end