Skip to content
This repository has been archived by the owner on Jun 1, 2023. It is now read-only.

Commit

Permalink
initial implementation and tests for tunnel command
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerball committed Apr 23, 2019
1 parent 93efb36 commit a5877ce
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.rubocop-*
.tmp/*
./ngrok*
Empty file added .tmp/.gitkeep
Empty file.
13 changes: 11 additions & 2 deletions lib/shopify-cli/commands/tunnel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ module Commands
class Tunnel < ShopifyCli::Command
# subcommands :start, :stop

def call(_args, _name)
puts CLI::UI.fmt(self.class.help)
def call(args, _name)
subcommand = args.shift
task = ShopifyCli::Tasks::Tunnel.new
case subcommand
when 'start'
task.call(@ctx)
when 'stop'
task.stop(@ctx)
else
puts CLI::UI.fmt(self.class.help)
end
end

def self.help
Expand Down
6 changes: 5 additions & 1 deletion lib/shopify-cli/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ def write(fname, content)
end

def puts(*args)
Kernel.puts(*args)
Kernel.puts(CLI::UI.fmt(*args))
end

def spawn(*args)
Kernel.spawn(*args)
end

def method_missing(method, *args)
Expand Down
138 changes: 136 additions & 2 deletions lib/shopify-cli/tasks/tunnel.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,144 @@
require 'json'
require 'tempfile'
require 'shopify_cli'

module ShopifyCli
module Tasks
class Tunnel < ShopifyCli::Task
def call(ctx, *)
ctx.puts('success!')
class FetchUrlError < RuntimeError; end
class NgrokError < RuntimeError; end

PORT = 8081
DOWNLOAD_URLS = {
mac: 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-darwin-amd64.zip',
}
TIMEOUT = 5

def call(ctx)
@ctx = ctx
start
end

def stop(ctx)
@ctx = ctx
if running?
Process.kill(9, state[:pid])
FileUtils.rm(pid_file)
@ctx.puts("{{green:x}} ngrok tunnel stopped")
else
@ctx.puts("{{green:x}} ngrok tunnel not running")
end
end

def start
install

url = if running?
state[:url]
else
run
end
@ctx.puts("{{green:✔︎}} ngrok tunnel running at #{url}")
url
end

def run
pid = @ctx.spawn(ngrok_command, chdir: ShopifyCli::ROOT)
Process.detach(pid)
url = fetch_url
write_state(pid, url, Time.now)
url
end

private

def running?
if File.exist?(pid_file)
state = read_state
begin
Process.kill(0, state[:pid])
true
rescue Errno::ESRCH
false
rescue Errno::EPERM
false
end
else
false
end
end

def read_state
content = JSON.parse(File.open(pid_file).read)
{
pid: content['pid'],
url: content['url'],
time: content['time'],
}
end

def write_state(pid, url, time)
File.open(pid_file, 'w') do |f|
f.write({
pid: pid,
url: url,
time: time,
}.to_json)
end
end

def install
return if File.exist?(File.join(ShopifyCli::ROOT, 'ngrok'))
spinner = CLI::UI::SpinGroup.new
spinner.add('Installing ngrok...') do
zip_dest = File.join(ShopifyCli::ROOT, 'ngrok.zip')
unless File.exist?(zip_dest)
@ctx.system('curl', '-o', zip_dest, DOWNLOAD_URLS[:mac], chdir: ShopifyCli::ROOT)
end
@ctx.system('unzip', '-u', zip_dest, chdir: ShopifyCli::ROOT)
FileUtils.rm(zip_dest)
end
spinner.wait
end

def fetch_url
counter = 0
while counter < TIMEOUT
log_content = File.read(log)
result = log_content.match(/msg="started tunnel".*url=(https:\/\/.+)/)
return result[1] if result

counter += 1
sleep(1)
end

error = log_content.scan(/msg="command failed" err="([^"]+)"/).flatten
unless error.empty?
stop(@ctx)
raise NgrokError, error.first
end

raise FetchUrlError, "Unable to fetch external url"
end

def ngrok_command
"exec #{File.join(ShopifyCli::ROOT, 'ngrok')} http -log=stdout -log-level=debug #{PORT} > #{log}"
end

def log
@log ||= begin
fname = File.join(ShopifyCli::ROOT, '.tmp', 'ngrok.log')
FileUtils.touch(fname)
File.join(fname)
end
end

def pid_file
File.join(ShopifyCli::ROOT, '.tmp/ngrok.pid')
end

def state
@state ||= read_state
end
end
end
Expand Down
21 changes: 21 additions & 0 deletions test/commands/tunnel_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'test_helper'

module ShopifyCli
module Commands
class TunnelTest < MiniTest::Test
def setup
@command = ShopifyCli::Commands::Tunnel.new
end

def test_start
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:call)
@command.call(['start'], nil)
end

def test_stop
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:stop)
@command.call(['stop'], nil)
end
end
end
end
3 changes: 3 additions & 0 deletions test/fixtures/ngrok.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
t=2019-03-26T14:45:57-0400 lvl=dbug msg="start tunnel listen" obj=tunnels.session name="command_line (http)" proto=http opts="&{Hostname:example.ngrok.io Auth: Subdomain: HostHeaderRewrite:false LocalURLScheme:http}" err=nil
t=2019-03-26T14:45:57-0400 lvl=info msg="started tunnel" obj=tunnels name="command_line (http)" addr=http://localhost:8081 url=http://example.ngrok.io
t=2019-03-26T14:45:57-0400 lvl=info msg="started tunnel" obj=tunnels name=command_line addr=http://localhost:8081 url=https://example.ngrok.io
Empty file added test/fixtures/ngrok.pid
Empty file.
1 change: 1 addition & 0 deletions test/fixtures/ngrok_error.log
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
t=2019-01-04T10:03:57-0500 lvl=crit msg="command failed" err="Tunnel session failed: Your account may not run more than 8 tunnels over a single ngrok client session."
1 change: 0 additions & 1 deletion test/shopify-cli/command_registry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class CommandRegistryTest < MiniTest::Test

class FakeCommand < ShopifyCli::Command
prerequisite_task :fake_task
attr_accessor :ctx

def call(_args, _name)
@ctx.puts('command!')
Expand Down
72 changes: 72 additions & 0 deletions test/task/tunnel_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require 'test_helper'

module ShopifyCli
module Tasks
class TunnelTest < MiniTest::Test
include TestHelpers::Context

def setup
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:pid_file).returns(pid_path)
super
FakeFS::FileSystem.clone(pid_path)
end

def test_start_running_returns_url
stub_binary
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:running?).returns(true)
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:state).returns(
url: 'https://example.ngrok.io',
)
assert_equal 'https://example.ngrok.io', ShopifyCli::Tasks::Tunnel.new.call(@context)
end

def test_start_not_running_returns_starts_ngrok
binary = stub_binary
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:running?).returns(false)
with_log do |log_path|
@context.expects(:spawn).with(
"exec #{binary} http -log=stdout -log-level=debug 8081 > #{log_path}",
chdir: ShopifyCli::ROOT
).returns(1000)
Process.expects(:detach).with(1000)
@context.expects(:puts).with(
"{{green:✔︎}} ngrok tunnel running at https://example.ngrok.io"
)
assert_equal 'https://example.ngrok.io', ShopifyCli::Tasks::Tunnel.new.call(@context)
end
end

def test_start_raises_error_on_ngrok_failure
binary = stub_binary
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:running?).returns(false)
with_log('ngrok_error') do |log_path|
@context.expects(:spawn).with(
"exec #{binary} http -log=stdout -log-level=debug 8081 > #{log_path}",
chdir: ShopifyCli::ROOT
).returns(1000)
assert_raises ShopifyCli::Tasks::Tunnel::NgrokError do
ShopifyCli::Tasks::Tunnel.new.call(@context)
end
end
end

def with_log(fixture = 'ngrok')
log_path = File.join(File.absolute_path(File.dirname(__FILE__)), "../fixtures/#{fixture}.log")
FakeFS::FileSystem.clone(log_path)
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:log).returns(log_path)
yield(log_path)
end

def stub_binary
ShopifyCli::Tasks::Tunnel.any_instance.stubs(:install)
binary = File.join(ShopifyCli::ROOT, 'ngrok')
FakeFS::FileSystem.clone(binary)
binary
end

def pid_path
@pid_path ||= File.join(File.absolute_path(File.dirname(__FILE__)), '../fixtures/ngrok.pid')
end
end
end
end

0 comments on commit a5877ce

Please sign in to comment.