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
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: CI

on:
pull_request:
push:
branches:
- master

concurrency:
group: ${{ github.ref_name }}-${{ github.workflow }}
cancel-in-progress: true

jobs:
test:
name: Tests
uses: theforeman/actions/.github/workflows/smart_proxy_plugin.yml@v1
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
smart_proxy_shellhooks-*.gem
/.idea
/Gemfile.lock
/.bundle
/.vendor
/logs
.ruby-version*
11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

source 'https://rubygems.org'
gemspec

group :test do
gem 'mocha'
gem 'rake'
gem 'smart_proxy', github: 'theforeman/smart-proxy', branch: ENV.fetch('SMART_PROXY_BRANCH', 'develop')
gem 'test-unit'
end
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

Provides an endpoint for foreman_webhooks plugin executing binaries (shell scripts, python scripts, anything).

## Running tests

Install the test dependencies and run the suite:

bundle install
bundle exec rake test

`bundle exec rake` also runs the tests. The test bundle uses Smart Proxy's
`develop` branch by default; set `SMART_PROXY_BRANCH` to test against another
branch. CI uses Foreman's shared Smart Proxy plugin workflow.

## Installation

Install the plugin using the foreman-installer. Never enable this service via HTTP endpoint, only HTTPS with authentication using client certificate and with trusted hosts should be used.
Expand Down
15 changes: 15 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

require 'rake'
require 'rake/testtask'

desc 'Default: run unit tests.'
task default: :test

desc 'Test the Smart Proxy Shell Hooks plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << '.'
t.libs << 'lib'
t.libs << 'test'
t.test_files = FileList['test/**/*_test.rb']
end
5 changes: 5 additions & 0 deletions lib/smart_proxy_shellhooks/shellhooks_api.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'sinatra'
require 'rack/rewindable_input'
require 'smart_proxy_shellhooks/shellhooks'

module Proxy::ShellHooks
Expand All @@ -8,6 +9,9 @@ class Api < ::Sinatra::Base
include ::Proxy::Log
helpers ::Proxy::Helpers

# Rack 3 inputs need buffering so parameter parsing does not consume the payload.
use Rack::RewindableInput::Middleware if defined?(Rack::RewindableInput::Middleware)

authorize_with_ssl_client
authorize_with_trusted_hosts

Expand Down Expand Up @@ -39,6 +43,7 @@ class Api < ::Sinatra::Base
break
end
end
# Sinatra may have already read the body while parsing form parameters.
request.body.rewind
Proxy::Util::CommandTask.new(cmd, request.body.read).start
end
Expand Down
1 change: 1 addition & 0 deletions smart_proxy_shellhooks.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ Gem::Specification.new do |s|
s.files = Dir['{lib,settings.d,bundler.d,examples}/**/*'] + s.extra_rdoc_files
s.homepage = 'http://github.com/theforeman/smart_proxy_shellhooks'
s.license = 'GPL-3.0-or-later'
s.required_ruby_version = '>= 3.0'
end
92 changes: 92 additions & 0 deletions test/shellhooks_api_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require_relative 'test_helper'
require 'stringio'
require 'tmpdir'
require 'fileutils'
require 'rack/mock'
require 'rackup/handler/webrick' if Rack.release.start_with?('3.')

class ShellhooksApiTest < Test::Unit::TestCase
# Implements the Rack 3 input interface without rewind.
class ForwardOnlyInput
def initialize(body)
@io = StringIO.new(body)
end

def read(*args)
@io.read(*args)
end

def gets(*args)
@io.gets(*args)
end

def each(&block)
@io.each(&block)
end
end

def setup
@directory = Dir.mktmpdir('shellhooks')
@script = File.join(@directory, 'print_body')
File.write(@script, "#!/bin/sh\ncat\n")
File.chmod(0700, @script)
Proxy::ShellHooks::Plugin.load_test_settings(:directory => @directory)
Proxy::ShellHooks::Api.any_instance.stubs(:logger).returns(stub(:debug => nil))
end

def teardown
FileUtils.remove_entry(@directory) if @directory
end

def test_plain_text_payload
assert_payload("hello\nworld\n", 'text/plain')
end

def test_json_payload
assert_payload('{"message":"hello"}', 'application/json')
end

def test_form_payload_after_parameter_parsing
assert_payload('message=hello+world&name=another_hook', 'application/x-www-form-urlencoded')
end

def test_empty_payload
assert_payload('', 'text/plain')
end

def test_rewindable_input
assert_payload('message=hello', 'application/x-www-form-urlencoded', StringIO.new('message=hello'))
end

if Rack.release.start_with?('3.')
def test_webrick_input
payload = 'message=hello+world'
request = stub('WEBrick request')
request.stubs(:body).yields(payload)
input = Rackup::Handler::WEBrick::Input.new(request)
assert_payload(payload, 'application/x-www-form-urlencoded', input)
end
end

private

def assert_payload(payload, content_type, input = nil)
# Rack 2 requires rewindable input; Rack 3 permits forward-only streams.
input ||= Rack.release.start_with?('2.') ? StringIO.new(payload) : ForwardOnlyInput.new(payload)
task = mock('command task')
task.expects(:start).returns('')
Proxy::Util::CommandTask.expects(:new).with([@script, 'an argument'], payload).returns(task)

env = Rack::MockRequest.env_for('/print_body',
:method => 'POST',
'CONTENT_TYPE' => content_type,
'CONTENT_LENGTH' => payload.bytesize.to_s,
'HTTP_X_SHELLHOOK_ARG_1' => 'an argument')
env['rack.input'] = input
status, _, body = Proxy::ShellHooks::Api.call(env)
assert_equal 200, status
ensure
body.close if body.respond_to?(:close)
env['rack.input'].close if env && env['rack.input'].respond_to?(:close)
end
end
11 changes: 11 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ENV['RACK_ENV'] = 'test'

require 'test/unit'
require 'mocha/test_unit'
require 'smart_proxy_for_testing'

# Keep test logs in the plugin directory, as in other Smart Proxy plugins.
FileUtils.mkdir_p File.dirname(Proxy::SETTINGS.log_file)

require 'smart_proxy_shellhooks'
require 'smart_proxy_shellhooks/shellhooks_api'
Loading