Skip to content

Commit d606cb1

Browse files
authored
Fix tests
1 parent 7999ab7 commit d606cb1

13 files changed

Lines changed: 799 additions & 109 deletions

.github/workflows/publish-to-rubygems.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ on:
99
branches: [main, master]
1010

1111
jobs:
12-
# test:
13-
# name: Test
14-
# runs-on: ubuntu-latest
15-
# steps:
16-
# - uses: actions/checkout@v4
17-
#
18-
# - uses: ruby/setup-ruby@v1
19-
# with:
20-
# ruby-version: "2.6"
21-
# bundler-cache: true
22-
#
23-
# - name: Run tests
24-
# run: bundle exec rspec spec --format progress
12+
test:
13+
name: Test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: ruby/setup-ruby@v1
19+
with:
20+
ruby-version: "2.6"
21+
bundler-cache: true
22+
23+
- name: Run tests
24+
run: bundle exec rspec spec --format progress
2525

2626
publish:
2727
name: Build & Publish gem
28-
# needs: test
28+
needs: test
2929
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
3030
runs-on: ubuntu-latest
3131
permissions:
@@ -47,4 +47,4 @@ jobs:
4747
uses: rubygems/configure-rubygems-credentials@v1.0.0
4848

4949
- name: Publish gem
50-
run: gem push lara-sdk-*.gem
50+
run: gem push lara-sdk-*.gem

spec/fixtures/audio.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"content": {
3+
"id": "aud_4Ef5Gh6Ij7Kl8Mn9Op0Qr",
4+
"status": "translated",
5+
"target": "it",
6+
"filename": "test.mp3",
7+
"source": "en-US",
8+
"created_at": "2024-01-15T10:00:00Z",
9+
"updated_at": "2024-01-15T10:05:00Z",
10+
"options": null,
11+
"translated_seconds": 120.5,
12+
"total_seconds": 120.5,
13+
"error_reason": null
14+
}
15+
}

spec/lara/audio_spec.rb

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# frozen_string_literal: true
2+
3+
require "tempfile"
4+
require "spec_helper"
5+
6+
RSpec.describe Lara::AudioTranslator do
7+
let(:base_url) { Lara::Client::DEFAULT_BASE_URL }
8+
let(:credentials) { Lara::Credentials.new("test-id", "test-secret") }
9+
let(:client) { Lara::Client.new(credentials, base_url: base_url) }
10+
let(:s3_double) do
11+
double("S3Client").tap do |s3|
12+
allow(s3).to receive(:upload).and_return(nil)
13+
allow(s3).to receive(:download).and_return("translated audio bytes")
14+
end
15+
end
16+
let(:audio) { described_class.new(client, s3_double) }
17+
18+
let(:audio_id) { "aud_4Ef5Gh6Ij7Kl8Mn9Op0Qr" }
19+
20+
def audio_content
21+
api_content_fixture("audio")
22+
end
23+
24+
describe "#upload" do
25+
it "fetches upload-url, uploads to S3, posts audio and returns Audio" do
26+
upload_url_response = { "url" => "https://s3-fake.example.com/upload", "fields" => { "key" => "s3key-1" } }
27+
stub_request(:get, "#{base_url}/v2/audio/upload-url")
28+
.with(query: hash_including({}))
29+
.to_return(
30+
status: 200,
31+
body: { "content" => upload_url_response }.to_json,
32+
headers: { "Content-Type" => "application/json" }
33+
)
34+
stub_request(:post, "#{base_url}/v2/audio/translate").to_return(
35+
status: 200,
36+
body: { "content" => audio_content }.to_json,
37+
headers: { "Content-Type" => "application/json" }
38+
)
39+
Tempfile.create(["audio", ".mp3"]) do |f|
40+
f.write("audio content")
41+
f.rewind
42+
result = audio.upload(file_path: f.path, filename: "test.mp3", target: "it", source: "en")
43+
expect(result).to be_a(Lara::Models::Audio)
44+
expect(result.id).to eq(audio_id)
45+
expect(result.status).to eq("translated")
46+
expect(s3_double).to have_received(:upload).with(url: upload_url_response["url"],
47+
fields: upload_url_response["fields"], io: f.path)
48+
end
49+
end
50+
51+
it "sends X-No-Trace when no_trace true" do
52+
upload_url_response = { "url" => "https://s3-fake.example.com/upload", "fields" => { "key" => "k1" } }
53+
stub_request(:get, "#{base_url}/v2/audio/upload-url")
54+
.with(query: hash_including({}))
55+
.to_return(
56+
status: 200,
57+
body: { "content" => upload_url_response }.to_json,
58+
headers: { "Content-Type" => "application/json" }
59+
)
60+
stub_request(:post, "#{base_url}/v2/audio/translate").to_return(
61+
status: 200,
62+
body: { "content" => audio_content }.to_json,
63+
headers: { "Content-Type" => "application/json" }
64+
)
65+
Tempfile.create(["audio", ".mp3"]) do |f|
66+
f.rewind
67+
audio.upload(file_path: f.path, filename: "x.mp3", target: "it", no_trace: true)
68+
expect(WebMock).to have_requested(:post,
69+
"#{base_url}/v2/audio/translate").with(headers: { "X-No-Trace" => "true" })
70+
end
71+
end
72+
end
73+
74+
describe "#status" do
75+
it "returns Audio" do
76+
stub_request(:get, "#{base_url}/v2/audio/#{audio_id}").to_return(
77+
status: 200,
78+
body: { "content" => audio_content }.to_json,
79+
headers: { "Content-Type" => "application/json" }
80+
)
81+
result = audio.status(audio_id)
82+
expect(result).to be_a(Lara::Models::Audio)
83+
expect(result.id).to eq(audio_id)
84+
end
85+
end
86+
87+
describe "#download" do
88+
it "fetches download-url and returns S3 download body" do
89+
download_url = "https://s3-fake.example.com/download/#{audio_id}"
90+
stub_request(:get, "#{base_url}/v2/audio/#{audio_id}/download-url").to_return(
91+
status: 200,
92+
body: { "content" => { "url" => download_url } }.to_json,
93+
headers: { "Content-Type" => "application/json" }
94+
)
95+
result = audio.download(audio_id)
96+
expect(result).to eq("translated audio bytes")
97+
expect(s3_double).to have_received(:download).with(url: download_url)
98+
end
99+
end
100+
101+
describe "#translate" do
102+
it "uploads, polls until translated, downloads and returns bytes" do
103+
upload_url_response = { "url" => "https://s3-fake.example.com/upload", "fields" => { "key" => "k1" } }
104+
download_url = "https://s3-fake.example.com/download/#{audio_id}"
105+
stub_request(:get, "#{base_url}/v2/audio/upload-url")
106+
.with(query: hash_including({}))
107+
.to_return(
108+
status: 200,
109+
body: { "content" => upload_url_response }.to_json,
110+
headers: { "Content-Type" => "application/json" }
111+
)
112+
stub_request(:post, "#{base_url}/v2/audio/translate").to_return(
113+
status: 200,
114+
body: { "content" => audio_content.merge("status" => "translated") }.to_json,
115+
headers: { "Content-Type" => "application/json" }
116+
)
117+
stub_request(:get, "#{base_url}/v2/audio/#{audio_id}").to_return(
118+
status: 200,
119+
body: { "content" => audio_content.merge("status" => "translated") }.to_json,
120+
headers: { "Content-Type" => "application/json" }
121+
)
122+
stub_request(:get, "#{base_url}/v2/audio/#{audio_id}/download-url").to_return(
123+
status: 200,
124+
body: { "content" => { "url" => download_url } }.to_json,
125+
headers: { "Content-Type" => "application/json" }
126+
)
127+
audio.instance_variable_set(:@polling_interval, 0)
128+
Tempfile.create(["audio", ".mp3"]) do |f|
129+
f.rewind
130+
result = audio.translate(file_path: f.path, filename: "test.mp3", target: "it")
131+
expect(result).to eq("translated audio bytes")
132+
end
133+
end
134+
135+
it "raises LaraApiError when status becomes error" do
136+
upload_url_response = { "url" => "https://s3-fake.example.com/upload", "fields" => { "key" => "k1" } }
137+
stub_request(:get, "#{base_url}/v2/audio/upload-url")
138+
.with(query: hash_including({}))
139+
.to_return(
140+
status: 200,
141+
body: { "content" => upload_url_response }.to_json,
142+
headers: { "Content-Type" => "application/json" }
143+
)
144+
stub_request(:post, "#{base_url}/v2/audio/translate").to_return(
145+
status: 200,
146+
body: { "content" => audio_content.merge("status" => "initialized") }.to_json,
147+
headers: { "Content-Type" => "application/json" }
148+
)
149+
stub_request(:get, "#{base_url}/v2/audio/#{audio_id}").to_return(
150+
status: 200,
151+
body: { "content" => audio_content.merge("status" => "error",
152+
"error_reason" => "Audio processing failed") }.to_json,
153+
headers: { "Content-Type" => "application/json" }
154+
)
155+
audio.instance_variable_set(:@polling_interval, 0)
156+
Tempfile.create(["audio", ".mp3"]) do |f|
157+
f.rewind
158+
expect { audio.translate(file_path: f.path, filename: "test.mp3", target: "it") }
159+
.to raise_error(Lara::LaraApiError, /Audio processing failed/)
160+
end
161+
end
162+
end
163+
end

spec/lara/client_spec.rb

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
let(:base_url) { "https://api.laratranslate.com" }
88
let(:client) { described_class.new(credentials, base_url: base_url) }
99

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",
1111
status: 200)
1212
url = if path.start_with?("http")
1313
path
1414
else
1515
"#{base_url}#{path.start_with?('/') ? path : "/#{path}"}"
1616
end
17-
stub_request(:post, url).to_return(
17+
stub_request(method.downcase.to_sym, url).to_return(
1818
status: status,
1919
body: response_body.is_a?(Hash) ? response_body.to_json : response_body,
2020
headers: { "Content-Type" => content_type }
@@ -42,7 +42,7 @@ def stub_api(method_override, path, response_body:, content_type: "application/j
4242
it "normalizes path with leading slash" do
4343
stub_api("GET", "/languages", response_body: { "content" => [] })
4444
client.get("languages")
45-
expect(WebMock).to have_requested(:post, "#{base_url}/languages")
45+
expect(WebMock).to have_requested(:get, "#{base_url}/languages")
4646
end
4747
end
4848

@@ -86,26 +86,102 @@ def stub_api(method_override, path, response_body:, content_type: "application/j
8686

8787
it "returns raw body for CSV content-type" do
8888
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" })
9193
result = client.get("/glossaries/#{glossary_id}/export", params: { content_type: "csv/table-uni" })
9294
expect(result).to eq("term,translation\nhello,ciao")
9395
end
9496
end
9597

9698
describe "request headers" do
97-
it "sends X-HTTP-Method-Override for get" do
99+
it "uses correct HTTP method for get requests" do
98100
stub_api("GET", "/languages", response_body: { "content" => [] })
99101
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")
102103
end
103104

104-
it "sends Authorization with Lara prefix" do
105+
it "sends Authorization with Bearer prefix" do
105106
stub_api("POST", "/translate", response_body: { "content" => {} })
106107
client.post("/translate", body: { q: "x", target: "it" })
107108
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")
109185
end
110186
end
111187
end

0 commit comments

Comments
 (0)