Skip to content

Commit 9400ee2

Browse files
committed
config, request assertion
1 parent 786f97c commit 9400ee2

4 files changed

Lines changed: 194 additions & 21 deletions

File tree

clients/cli/spec/cli_spec.rb

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,50 @@
22
require "spec_helper"
33

44
RSpec.describe "General behavior" do
5+
let(:config) { <<~YAML }
6+
phrase:
7+
host: #{ENV.fetch("BASE_URL")}
8+
project_id: "test-project"
9+
access_token: "test-token"
10+
YAML
11+
512
it "info prints the version" do
613
r = run_cli("info")
714
expect(r[:exit_code]).to eq(0)
815
expect(r[:stdout]).to include("Phrase Strings client version")
916
expect(r[:stderr]).to include("You're running a development version")
1017
end
1118

12-
it "handles server 500s cleanly" do
13-
# Example: pass a flag so your CLI requests /status/500, if it supports it.
14-
# Otherwise adapt your rack app to respond 500 for the request your CLI already makes.
15-
r = run_cli("call", "/status/500")
16-
expect(r[:exit_code]).not_to eq(0)
17-
expect(r[:stderr]).to match(/500|error/i)
19+
describe "format commands" do
20+
before do
21+
mock_set!("GET", "/formats", status: 200, body: [
22+
{
23+
name: "Ruby/Rails YAML",
24+
api_name: "yml",
25+
description: "YAML file format for use with Ruby/Rails applications",
26+
extension: "yml",
27+
default_encoding: "UTF-8",
28+
importable: true,
29+
exportable: true,
30+
default_file: "./config/locales/<locale_name>.yml",
31+
renders_default_locale: false,
32+
includes_locale_information: true
33+
}
34+
])
35+
end
36+
37+
it "gets the format list" do
38+
r = run_cli("formats", "list", config:)
39+
expect(r[:exit_code]).to eq(0)
40+
expect(r[:stdout]).to include("Ruby/Rails YAML")
41+
42+
requests_made = mock_requests
43+
expect(requests_made.length).to eq(1)
44+
45+
request = requests_made.first
46+
expect(request["method"]).to eq("GET")
47+
expect(request["path"]).to eq("/formats")
48+
expect(request["headers"]["HTTP_AUTHORIZATION"]).to eq("token test-token")
49+
end
1850
end
1951
end

clients/cli/spec/spec_helper.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
require "open3"
33
require "socket"
44
require "timeout"
5+
require "tempfile"
6+
7+
require_relative "support/mock_control"
58

69
def free_port
710
s = TCPServer.new("127.0.0.1", 0)
@@ -59,19 +62,24 @@ def free_port
5962
io.close rescue nil
6063
end
6164
end
65+
66+
config.include MockControl
6267
end
6368

6469
# Point this to your actual binary (relative to repo root, or absolute path)
6570
CLI_PATH = File.expand_path("../phrase-cli", __dir__)
6671
# e.g. if it’s built somewhere: File.expand_path("../../target/release/my_cli", __dir__)
6772

68-
def run_cli(*args, env: {})
69-
full_env = {
70-
# Whatever your CLI reads to find the server:
71-
# BASE_URL is set in spec_helper, but you can override per test via env:
72-
"BASE_URL" => ENV.fetch("BASE_URL")
73-
}.merge(env)
73+
def run_cli(*args, config: nil, env: {})
74+
stdout, stderr, status = if config.nil?
75+
Open3.capture3({}, CLI_PATH, *args.map(&:to_s))
76+
else
77+
Tempfile.create(".phrase.yml") do |f|
78+
f.write(config)
79+
f.flush
7480

75-
stdout, stderr, status = Open3.capture3(full_env, CLI_PATH, *args.map(&:to_s))
81+
Open3.capture3({}, CLI_PATH, "--config", f.path, *args.map(&:to_s))
82+
end
83+
end
7684
{ stdout: stdout, stderr: stderr, status: status, exit_code: status.exitstatus }
7785
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require "net/http"
2+
require "json"
3+
require "uri"
4+
5+
module MockControl
6+
def mock_reset!
7+
post_json("/__control__/reset", {})
8+
end
9+
10+
def mock_set!(method, path, status:, body:, headers: {})
11+
post_json("/__control__/set", {
12+
method: method,
13+
path: path,
14+
status: status,
15+
headers: headers,
16+
body: body
17+
})
18+
end
19+
20+
def mock_requests
21+
get_json("/__control__/requests").fetch("requests")
22+
end
23+
24+
def mock_clear_requests!
25+
post_json("/__control__/requests/clear", {})
26+
end
27+
28+
private
29+
30+
def base_uri
31+
URI(ENV.fetch("BASE_URL"))
32+
end
33+
34+
def post_json(path, payload)
35+
uri = URI.join(base_uri.to_s, path)
36+
req = Net::HTTP::Post.new(uri)
37+
req["content-type"] = "application/json"
38+
req.body = JSON.dump(payload)
39+
40+
Net::HTTP.start(uri.host, uri.port) do |http|
41+
res = http.request(req)
42+
raise "Mock control failed: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
43+
end
44+
end
45+
46+
def get_json(path)
47+
uri = URI.join(base_uri.to_s, path)
48+
res = Net::HTTP.get_response(uri)
49+
raise "Mock control failed: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
50+
JSON.parse(res.body)
51+
end
52+
end
Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,95 @@
11
require "json"
22

3+
$mock = {
4+
routes: {},
5+
requests: [] # array of hashes
6+
}
7+
8+
def json(status, obj, headers: {})
9+
[status, { "content-type" => "application/json" }.merge(headers), [JSON.dump(obj)]]
10+
end
11+
12+
def read_body(env)
13+
io = env["rack.input"]
14+
return "" unless io
15+
body = io.read
16+
io.rewind if io.respond_to?(:rewind)
17+
body
18+
end
19+
20+
def record_request!(env)
21+
method = env["REQUEST_METHOD"]
22+
path = env["PATH_INFO"]
23+
query = env["QUERY_STRING"]
24+
body = read_body(env)
25+
26+
# Record a safe subset of headers (HTTP_... keys + content-type/length)
27+
headers = env.each_with_object({}) do |(k, v), h|
28+
next unless k.start_with?("HTTP_") || k == "CONTENT_TYPE" || k == "CONTENT_LENGTH"
29+
h[k] = v
30+
end
31+
32+
$mock[:requests] << {
33+
method: method,
34+
path: path,
35+
query: query,
36+
headers: headers,
37+
body: body
38+
}
39+
end
40+
341
run lambda { |env|
4-
path = env["PATH_INFO"]
42+
method = env["REQUEST_METHOD"]
43+
path = env["PATH_INFO"]
44+
45+
# ---- Control API ----
46+
if method == "POST" && path == "/__control__/reset"
47+
$mock[:routes].clear
48+
$mock[:requests].clear
49+
return json(200, { ok: true })
50+
end
51+
52+
if method == "POST" && path == "/__control__/set"
53+
payload = JSON.parse(read_body(env))
54+
m = payload.fetch("method")
55+
p = payload.fetch("path")
556

6-
case path
7-
when "/ping"
8-
[200, { "Content-Type" => "application/json" }, [JSON.dump({ message: "pong" })]]
9-
when "/status/500"
10-
[500, { "Content-Type" => "application/json" }, [JSON.dump({ error: "boom" })]]
11-
else
12-
[404, { "Content-Type" => "application/json" }, [JSON.dump({ error: "not found" })]]
57+
$mock[:routes][[m, p]] = {
58+
status: payload.fetch("status"),
59+
headers: payload["headers"] || {},
60+
body: payload["body"] || ""
61+
}
62+
63+
return json(200, { ok: true })
64+
end
65+
66+
if method == "GET" && path == "/__control__/requests"
67+
return json(200, { requests: $mock[:requests] })
1368
end
69+
70+
if method == "POST" && path == "/__control__/requests/clear"
71+
$mock[:requests].clear
72+
return json(200, { ok: true })
73+
end
74+
75+
# ---- Record every non-control request ----
76+
record_request!(env)
77+
78+
# ---- Serve mocked responses ----
79+
route = $mock[:routes][[method, path]]
80+
return json(404, { error: "not mocked", method: method, path: path }) unless route
81+
82+
status = route[:status]
83+
headers = route[:headers] || {}
84+
body = route[:body]
85+
86+
body_str =
87+
case body
88+
when String then body
89+
else JSON.dump(body)
90+
end
91+
92+
headers = { "content-type" => "application/json" }.merge(headers) if body.is_a?(Hash) || body.is_a?(Array)
93+
94+
[status, headers, [body_str]]
1495
}

0 commit comments

Comments
 (0)