Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/test-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
paths:
- .github/workflows/test-cli.yml
- openapi-generator/cli_lang.yaml
- clients/cli/*
- 'clients/cli/**'
jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -17,7 +17,7 @@ jobs:
uses: actions/setup-go@v2
with:
go-version: '1.24.4'
- name: Generate CLI and run tests
- name: Generate CLI and run unit tests
env:
GOPRIVATE: github.com/phrase/phrase-go
run: |
Expand All @@ -28,6 +28,16 @@ jobs:
npm run generate.cli
go build .
go test -v ./...
- name: Install Ruby and rspec
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ./clients/cli
- name: Run integration tests
run: |
cd ./clients/cli
bundle exec rspec
- name: License check
uses: phrase/actions/lawa-ci@v1
with:
Expand Down
6 changes: 6 additions & 0 deletions clients/cli/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "https://rubygems.org"

gem "rspec"
gem "rack"
gem "rackup"
gem "webrick"
33 changes: 33 additions & 0 deletions clients/cli/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.6.2)
rack (3.2.4)
rackup (2.3.1)
rack (>= 3)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
webrick (1.9.2)

PLATFORMS
x86_64-linux

DEPENDENCIES
rack
rackup
rspec
webrick

BUNDLED WITH
2.4.12
52 changes: 52 additions & 0 deletions clients/cli/spec/cli_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "open3"
require "spec_helper"

RSpec.describe "General behavior" do
let(:token) { "test-token" }
let(:config) { <<~YAML }
phrase:
host: #{ENV.fetch("BASE_URL")}
project_id: "test-project"
access_token: "#{token}"
YAML

it "info prints the version" do
r = run_cli("info")
expect(r[:exit_code]).to eq(0)
expect(r[:stdout]).to include("Phrase Strings client version")
expect(r[:stderr]).to include("You're running a development version")
end

describe "a simple command" do
before do
mock_set!("GET", "/formats", status: 200, body: [
{
name: "Ruby/Rails YAML",
api_name: "yml",
description: "YAML file format for use with Ruby/Rails applications",
extension: "yml",
default_encoding: "UTF-8",
importable: true,
exportable: true,
default_file: "./config/locales/<locale_name>.yml",
renders_default_locale: false,
includes_locale_information: true
}
])
end

it "performs the authenticated request" do
r = run_cli("formats", "list", config:)
expect(r[:exit_code]).to eq(0)
expect(r[:stdout]).to include("Ruby/Rails YAML")

requests_made = mock_requests
expect(requests_made.length).to eq(1)

request = requests_made.first
expect(request["method"]).to eq("GET")
expect(request["path"]).to eq("/formats")
expect(request["headers"]["HTTP_AUTHORIZATION"]).to eq("token #{token}")
end
end
end
85 changes: 85 additions & 0 deletions clients/cli/spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "rspec"
require "open3"
require "socket"
require "timeout"
require "tempfile"

require_relative "support/mock_control"

def free_port
s = TCPServer.new("127.0.0.1", 0)
port = s.addr[1]
s.close
port
end

RSpec.configure do |config|
config.before(:suite) do
@mock_port = free_port
@mock_url = "http://127.0.0.1:#{@mock_port}"
puts "Starting mock server at #{@mock_url}"

ru_path = File.expand_path("support/mock_server.ru", __dir__)

# Start rackup in background
@rack_stdin, @rack_stdout, @rack_stderr, @rack_wait_thr =
Open3.popen3(
"bundle", "exec", "rackup",
"--host", "127.0.0.1",
"--port", @mock_port.to_s,
ru_path
)

# Wait until the server is accepting connections
Timeout.timeout(5) do
loop do
begin
TCPSocket.new("127.0.0.1", @mock_port).close
break
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
sleep 0.05
end
end
end

# Expose to CLI; adapt env var name to whatever your binary uses.
ENV["BASE_URL"] = @mock_url
end

config.after(:suite) do
ENV.delete("BASE_URL")

if @rack_wait_thr&.alive?
Process.kill("TERM", @rack_wait_thr.pid)
begin
Timeout.timeout(2) { @rack_wait_thr.join }
rescue Timeout::Error
Process.kill("KILL", @rack_wait_thr.pid)
end
end

[@rack_stdin, @rack_stdout, @rack_stderr].compact.each do |io|
io.close rescue nil
end
end

config.include MockControl
end

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

def run_cli(*args, config: nil, env: {})
stdout, stderr, status = if config.nil?
Open3.capture3(env, CLI_PATH, *args.map(&:to_s))
else
Tempfile.create(".phrase.yml") do |f|
f.write(config)
f.flush

Open3.capture3(env, CLI_PATH, "--config", f.path, *args.map(&:to_s))
end
end
{ stdout: stdout, stderr: stderr, status: status, exit_code: status.exitstatus }
end
52 changes: 52 additions & 0 deletions clients/cli/spec/support/mock_control.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "net/http"
require "json"
require "uri"

module MockControl
def mock_reset!
post_json("/__control__/reset", {})
end

def mock_set!(method, path, status:, body:, headers: {})
post_json("/__control__/set", {
method: method,
path: path,
status: status,
headers: headers,
body: body
})
end

def mock_requests
get_json("/__control__/requests").fetch("requests")
end

def mock_clear_requests!
post_json("/__control__/requests/clear", {})
end

private

def base_uri
URI(ENV.fetch("BASE_URL"))
end

def post_json(path, payload)
uri = URI.join(base_uri.to_s, path)
req = Net::HTTP::Post.new(uri)
req["content-type"] = "application/json"
req.body = JSON.dump(payload)

Net::HTTP.start(uri.host, uri.port) do |http|
res = http.request(req)
raise "Mock control failed: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
end
end

def get_json(path)
uri = URI.join(base_uri.to_s, path)
res = Net::HTTP.get_response(uri)
raise "Mock control failed: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)
end
end
95 changes: 95 additions & 0 deletions clients/cli/spec/support/mock_server.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require "json"

$mock = {
routes: {},
requests: [] # array of hashes
}

def json(status, obj, headers: {})
[status, { "content-type" => "application/json" }.merge(headers), [JSON.dump(obj)]]
end

def read_body(env)
io = env["rack.input"]
return "" unless io
body = io.read
io.rewind if io.respond_to?(:rewind)
body
end

def record_request!(env)
method = env["REQUEST_METHOD"]
path = env["PATH_INFO"]
query = env["QUERY_STRING"]
body = read_body(env)

# Record a safe subset of headers (HTTP_... keys + content-type/length)
headers = env.each_with_object({}) do |(k, v), h|
next unless k.start_with?("HTTP_") || k == "CONTENT_TYPE" || k == "CONTENT_LENGTH"
h[k] = v
end

$mock[:requests] << {
method: method,
path: path,
query: query,
headers: headers,
body: body
}
end

run lambda { |env|
method = env["REQUEST_METHOD"]
path = env["PATH_INFO"]

# ---- Control API ----
if method == "POST" && path == "/__control__/reset"
$mock[:routes].clear
$mock[:requests].clear
return json(200, { ok: true })
end

if method == "POST" && path == "/__control__/set"
payload = JSON.parse(read_body(env))
m = payload.fetch("method")
p = payload.fetch("path")

$mock[:routes][[m, p]] = {
status: payload.fetch("status"),
headers: payload["headers"] || {},
body: payload["body"] || ""
}

return json(200, { ok: true })
end

if method == "GET" && path == "/__control__/requests"
return json(200, { requests: $mock[:requests] })
end

if method == "POST" && path == "/__control__/requests/clear"
$mock[:requests].clear
return json(200, { ok: true })
end

# ---- Record every non-control request ----
record_request!(env)

# ---- Serve mocked responses ----
route = $mock[:routes][[method, path]]
return json(404, { error: "not mocked", method: method, path: path }) unless route

status = route[:status]
headers = route[:headers] || {}
body = route[:body]

body_str =
case body
when String then body
else JSON.dump(body)
end

headers = { "content-type" => "application/json" }.merge(headers) if body.is_a?(Hash) || body.is_a?(Array)

[status, headers, [body_str]]
}
Loading