main
rb 111 lines 4.04 KB
Raw
1 # frozen_string_literal: true
2
3 require "rails_helper"
4
5 RSpec.describe WebSearchService do
6 # Builds a real Net::HTTPResponse (the class the adapter checks with
7 # Net::HTTPSuccess) carrying a canned body. The repo has no webmock/vcr, so
8 # specs stub the adapter's http_get seam with one of these.
9 def http_response(code, body)
10 klass = Net::HTTPResponse::CODE_TO_OBJ.fetch(code.to_s)
11 response = klass.new("1.1", code.to_s, nil)
12 response.instance_variable_set(:@read, true)
13 response.instance_variable_set(:@body, body)
14 response
15 end
16
17 def brave_body(results)
18 { "web" => { "results" => results } }.to_json
19 end
20
21 before do
22 allow(ENV).to receive(:[]).and_call_original
23 allow(ENV).to receive(:fetch).and_call_original
24 allow(ENV).to receive(:[]).with("BRAVE_SEARCH_API_KEY").and_return("brave-key")
25 end
26
27 describe ".provider" do
28 it "defaults to the Brave adapter" do
29 expect(described_class.provider).to be_a(WebSearchService::Brave)
30 end
31
32 it "raises a configuration error for an unknown SEARCH_PROVIDER" do
33 allow(ENV).to receive(:fetch).with("SEARCH_PROVIDER", "brave").and_return("bing")
34
35 expect { described_class.provider }
36 .to raise_error(WebSearchService::Error, /unknown search provider "bing".*SEARCH_PROVIDER.*brave/im)
37 end
38 end
39
40 describe ".search" do
41 it "clamps count to at most 10 before hitting the provider" do
42 adapter = instance_double(WebSearchService::Brave)
43 allow(described_class).to receive(:provider).and_return(adapter)
44 expect(adapter).to receive(:search).with("rust", count: 10).and_return([])
45
46 described_class.search("rust", count: 50)
47 end
48 end
49
50 describe WebSearchService::Brave do
51 subject(:adapter) { described_class.new }
52
53 it "requests the Brave endpoint with q, count, and the subscription token" do
54 captured_uri = nil
55 captured_key = nil
56 allow(adapter).to receive(:http_get) do |uri, api_key|
57 captured_uri = uri
58 captured_key = api_key
59 http_response(200, brave_body([]))
60 end
61
62 adapter.search("ruby net/http", count: 3)
63
64 expect(captured_uri.to_s).to start_with("https://api.search.brave.com/res/v1/web/search?")
65 expect(URI.decode_www_form(captured_uri.query).to_h)
66 .to eq("q" => "ruby net/http", "count" => "3")
67 expect(captured_key).to eq("brave-key")
68 end
69
70 it "parses web.results[] into {title, url, snippet}" do
71 body = brave_body([
72 { "title" => "Ruby", "url" => "https://ruby-lang.org", "description" => "A language.",
73 "extra" => "ignored" },
74 { "title" => "Rails", "url" => "https://rubyonrails.org", "description" => "A framework." }
75 ])
76 allow(adapter).to receive(:http_get).and_return(http_response(200, body))
77
78 expect(adapter.search("ruby", count: 5)).to eq([
79 { "title" => "Ruby", "url" => "https://ruby-lang.org", "snippet" => "A language." },
80 { "title" => "Rails", "url" => "https://rubyonrails.org", "snippet" => "A framework." }
81 ])
82 end
83
84 it "returns an empty list when the response has no web.results" do
85 allow(adapter).to receive(:http_get).and_return(http_response(200, "{}"))
86
87 expect(adapter.search("nothing", count: 5)).to eq([])
88 end
89
90 it "raises when BRAVE_SEARCH_API_KEY is not set, naming the env var" do
91 allow(ENV).to receive(:[]).with("BRAVE_SEARCH_API_KEY").and_return(nil)
92
93 expect { adapter.search("ruby", count: 5) }
94 .to raise_error(WebSearchService::Error, /BRAVE_SEARCH_API_KEY/)
95 end
96
97 it "surfaces an upstream HTTP failure with its status" do
98 allow(adapter).to receive(:http_get).and_return(http_response(429, "slow down"))
99
100 expect { adapter.search("ruby", count: 5) }
101 .to raise_error(WebSearchService::Error, /HTTP 429/)
102 end
103
104 it "raises a readable error for an unparseable body" do
105 allow(adapter).to receive(:http_get).and_return(http_response(200, "<html>nope</html>"))
106
107 expect { adapter.search("ruby", count: 5) }
108 .to raise_error(WebSearchService::Error, /unreadable response/)
109 end
110 end
111 end