|
7 | 7 | let(:base_url) { "https://api.laratranslate.com" } |
8 | 8 | let(:client) { described_class.new(credentials, base_url: base_url) } |
9 | 9 |
|
10 | | - def stub_api(method_override, path, response_body:, content_type: "application/json", |
| 10 | + def stub_api(method, path, response_body:, content_type: "application/json", |
11 | 11 | status: 200) |
12 | 12 | url = if path.start_with?("http") |
13 | 13 | path |
14 | 14 | else |
15 | 15 | "#{base_url}#{path.start_with?('/') ? path : "/#{path}"}" |
16 | 16 | end |
17 | | - stub_request(:post, url).to_return( |
| 17 | + stub_request(method.downcase.to_sym, url).to_return( |
18 | 18 | status: status, |
19 | 19 | body: response_body.is_a?(Hash) ? response_body.to_json : response_body, |
20 | 20 | headers: { "Content-Type" => content_type } |
@@ -42,7 +42,7 @@ def stub_api(method_override, path, response_body:, content_type: "application/j |
42 | 42 | it "normalizes path with leading slash" do |
43 | 43 | stub_api("GET", "/languages", response_body: { "content" => [] }) |
44 | 44 | client.get("languages") |
45 | | - expect(WebMock).to have_requested(:post, "#{base_url}/languages") |
| 45 | + expect(WebMock).to have_requested(:get, "#{base_url}/languages") |
46 | 46 | end |
47 | 47 | end |
48 | 48 |
|
@@ -86,26 +86,102 @@ def stub_api(method_override, path, response_body:, content_type: "application/j |
86 | 86 |
|
87 | 87 | it "returns raw body for CSV content-type" do |
88 | 88 | glossary_id = "gls_1Bc2De3Fg4Hi5Jk6Lm7No" |
89 | | - stub_api("GET", "/glossaries/#{glossary_id}/export", response_body: "term,translation\nhello,ciao", |
90 | | - content_type: "text/csv") |
| 89 | + stub_request(:get, "#{base_url}/glossaries/#{glossary_id}/export") |
| 90 | + .with(query: { "content_type" => "csv/table-uni" }) |
| 91 | + .to_return(status: 200, body: "term,translation\nhello,ciao", |
| 92 | + headers: { "Content-Type" => "text/csv" }) |
91 | 93 | result = client.get("/glossaries/#{glossary_id}/export", params: { content_type: "csv/table-uni" }) |
92 | 94 | expect(result).to eq("term,translation\nhello,ciao") |
93 | 95 | end |
94 | 96 | end |
95 | 97 |
|
96 | 98 | describe "request headers" do |
97 | | - it "sends X-HTTP-Method-Override for get" do |
| 99 | + it "uses correct HTTP method for get requests" do |
98 | 100 | stub_api("GET", "/languages", response_body: { "content" => [] }) |
99 | 101 | client.get("/languages") |
100 | | - expect(WebMock).to have_requested(:post, "#{base_url}/languages") |
101 | | - .with(headers: { "X-HTTP-Method-Override" => "GET" }) |
| 102 | + expect(WebMock).to have_requested(:get, "#{base_url}/languages") |
102 | 103 | end |
103 | 104 |
|
104 | | - it "sends Authorization with Lara prefix" do |
| 105 | + it "sends Authorization with Bearer prefix" do |
105 | 106 | stub_api("POST", "/translate", response_body: { "content" => {} }) |
106 | 107 | client.post("/translate", body: { q: "x", target: "it" }) |
107 | 108 | expect(WebMock).to(have_requested(:post, "#{base_url}/translate") |
108 | | - .with { |req| req.headers["Authorization"]&.start_with?("Lara test-id:") }) |
| 109 | + .with { |req| req.headers["Authorization"]&.start_with?("Bearer ") }) |
| 110 | + end |
| 111 | + end |
| 112 | + |
| 113 | + describe "authentication" do |
| 114 | + it "initializes with AuthToken and skips authenticate" do |
| 115 | + token = Lara::AuthToken.new("jwt-token", "refresh-token") |
| 116 | + c = described_class.new(token, base_url: base_url) |
| 117 | + stub_api("GET", "/test", response_body: { "content" => "ok" }) |
| 118 | + result = c.get("/test") |
| 119 | + expect(result).to eq("ok") |
| 120 | + expect(WebMock).not_to have_requested(:post, %r{/v2/auth}) |
| 121 | + end |
| 122 | + |
| 123 | + it "raises ArgumentError for invalid auth_method" do |
| 124 | + expect { described_class.new("invalid") }.to raise_error(ArgumentError, /auth_method/) |
| 125 | + end |
| 126 | + |
| 127 | + it "retries on 401 jwt expired by refreshing token" do |
| 128 | + stub_request(:post, "#{base_url}/test").to_return( |
| 129 | + { status: 401, |
| 130 | + body: { "error" => { "type" => "AuthError", "message" => "jwt expired" } }.to_json, |
| 131 | + headers: { "Content-Type" => "application/json" } }, |
| 132 | + { status: 200, |
| 133 | + body: { "content" => "success" }.to_json, |
| 134 | + headers: { "Content-Type" => "application/json" } } |
| 135 | + ) |
| 136 | + stub_request(:post, "#{base_url}/v2/auth/refresh").to_return( |
| 137 | + status: 200, |
| 138 | + body: { "token" => "new-jwt" }.to_json, |
| 139 | + headers: { "Content-Type" => "application/json", "x-lara-refresh-token" => "new-refresh" } |
| 140 | + ) |
| 141 | + result = client.post("/test", body: { q: "x" }) |
| 142 | + expect(result).to eq("success") |
| 143 | + end |
| 144 | + |
| 145 | + it "raises non-jwt-expired 401 without retrying" do |
| 146 | + stub_request(:post, "#{base_url}/test").to_return( |
| 147 | + status: 401, |
| 148 | + body: { "error" => { "type" => "AuthError", "message" => "invalid token" } }.to_json, |
| 149 | + headers: { "Content-Type" => "application/json" } |
| 150 | + ) |
| 151 | + expect { client.post("/test", body: { q: "x" }) }.to raise_error(Lara::LaraApiError) do |e| |
| 152 | + expect(e.status_code).to eq(401) |
| 153 | + expect(e.message).to include("invalid token") |
| 154 | + end |
| 155 | + end |
| 156 | + |
| 157 | + it "returns raw_response body when raw_response is true" do |
| 158 | + stub_request(:post, "#{base_url}/v2/images/translate").to_return( |
| 159 | + status: 200, |
| 160 | + body: "raw-binary-data", |
| 161 | + headers: { "Content-Type" => "image/png" } |
| 162 | + ) |
| 163 | + result = client.post("/v2/images/translate", body: { target: "it" }, raw_response: true) |
| 164 | + expect(result).to eq("raw-binary-data") |
| 165 | + end |
| 166 | + end |
| 167 | + |
| 168 | + describe "streaming" do |
| 169 | + it "parses NDJSON streaming response when callback given" do |
| 170 | + stream_body = "{\"content\":{\"translation\":\"partial\"}}\n{\"content\":{\"translation\":\"Ciao\"}}\n" |
| 171 | + stub_api("POST", "/translate", response_body: stream_body) |
| 172 | + results = [] |
| 173 | + client.post("/translate", body: { q: "Hello", target: "it", reasoning: true }) do |partial| |
| 174 | + results << partial |
| 175 | + end |
| 176 | + expect(results.size).to eq(2) |
| 177 | + expect(results.last).to eq("translation" => "Ciao") |
| 178 | + end |
| 179 | + |
| 180 | + it "returns last result from streaming response" do |
| 181 | + stream_body = "{\"content\":{\"translation\":\"Ciao\"}}\n" |
| 182 | + stub_api("POST", "/translate", response_body: stream_body) |
| 183 | + result = client.post("/translate", body: { q: "Hello", target: "it", reasoning: true }) |
| 184 | + expect(result).to eq("translation" => "Ciao") |
109 | 185 | end |
110 | 186 | end |
111 | 187 | end |
0 commit comments