Skip to content

Commit fca2d9f

Browse files
committed
Lamby Proxy Server for Local Development.
1 parent b53be2c commit fca2d9f

11 files changed

+174
-1
lines changed

Gemfile.lock

+2
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ GEM
170170
timeout (0.3.2)
171171
tzinfo (2.0.6)
172172
concurrent-ruby (~> 1.0)
173+
webrick (1.8.1)
173174
websocket-driver (0.7.5)
174175
websocket-extensions (>= 0.1.0)
175176
websocket-extensions (0.1.5)
@@ -189,6 +190,7 @@ DEPENDENCIES
189190
pry
190191
rails
191192
rake
193+
webrick
192194

193195
BUNDLED WITH
194196
2.3.26

Rakefile

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ require "rake/testtask"
55
Rake::TestTask.new(:test) do |t|
66
t.libs << "test"
77
t.libs << "lib"
8-
t.test_files = FileList["test/**/*_test.rb"]
8+
t.test_files = begin
9+
if ENV['TEST_FILE']
10+
[ENV['TEST_FILE']]
11+
else
12+
FileList["test/**/*_test.rb"]
13+
end
14+
end
915
t.verbose = false
1016
t.warning = false
1117
end

lamby.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
2626
spec.add_development_dependency 'minitest-focus'
2727
spec.add_development_dependency 'mocha'
2828
spec.add_development_dependency 'pry'
29+
spec.add_development_dependency 'webrick'
2930
end

lib/lamby.rb

+2
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,7 @@ def config
3535
end
3636

3737
autoload :SsmParameterStore, 'lamby/ssm_parameter_store'
38+
autoload :ProxyContext, 'lamby/proxy_context'
39+
autoload :ProxyServer, 'lamby/proxy_server'
3840

3941
end

lib/lamby/proxy_context.rb

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module Lamby
2+
# This class is used by the `lamby:proxy_server` Rake task to run a
3+
# Rack server for local development proxy. Specifically, this class
4+
# accepts a JSON respresentation of a Lambda context object converted
5+
# to a Hash as the single arugment.
6+
#
7+
class ProxyContext
8+
def initialize(data)
9+
@data = data
10+
end
11+
12+
def method_missing(method_name, *args, &block)
13+
key = method_name.to_s
14+
if @data.key?(key)
15+
@data[key]
16+
else
17+
super
18+
end
19+
end
20+
21+
def respond_to_missing?(method_name, include_private = false)
22+
@data.key?(method_name.to_s) || super
23+
end
24+
end
25+
end

lib/lamby/proxy_server.rb

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Lamby
2+
class ProxyServer
3+
4+
METHOD_NOT_ALLOWED = <<-HEREDOC.strip
5+
<h1>Method Not Allowed</h1>
6+
<p>Please POST to this endpoint with an application/json content type and JSON payload of your Lambda's event and context.<p>
7+
<p>Example: <code>{ "event": event, "context": context }</code></p>
8+
HEREDOC
9+
10+
def call(env)
11+
return method_not_allowed unless method_allowed?(env)
12+
event, context = event_and_context(env)
13+
Lamby.cmd event: event, context: context
14+
end
15+
16+
private
17+
18+
def event_and_context(env)
19+
data = env['rack.input'].dup.read
20+
json = JSON.parse(data)
21+
[ json['event'], Lamby::ProxyContext.new(json['context']) ]
22+
end
23+
24+
def method_allowed?(env)
25+
env['REQUEST_METHOD'] == 'POST' && env['CONTENT_TYPE'] == 'application/json'
26+
end
27+
28+
def method_not_allowed
29+
{ statusCode: 405,
30+
headers: {"Content-Type" => "text/html"},
31+
body: METHOD_NOT_ALLOWED.dup }
32+
end
33+
end
34+
end

lib/lamby/railtie.rb

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
module Lamby
22
class Railtie < ::Rails::Railtie
33
config.lamby = Lamby::Config.config
4+
5+
rake_tasks do
6+
load 'lamby/tasks.rake'
7+
end
48
end
59
end

lib/lamby/tasks.rake

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace :lamby do
2+
task :proxy_server => [:environment] do
3+
require 'webrick'
4+
port = ENV['LAMBY_PROXY_PORT'] || 3000
5+
Rack::Handler::WEBrick.run Lamby::ProxyServer.new, Port: port
6+
end
7+
end

test/proxy_context_test.rb

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'test_helper'
2+
3+
class ProxyContextTest < LambySpec
4+
5+
let(:context_data) { TestHelpers::LambdaContext.raw_data }
6+
let(:proxy_context) { Lamby::ProxyContext.new(context_data) }
7+
8+
it 'should respond to all context methods' do
9+
context_data.keys.each do |key|
10+
response = proxy_context.respond_to?(key.to_sym)
11+
expect(response).must_equal true, "Expected context to respond to #{key.inspect}"
12+
end
13+
end
14+
15+
it 'should return the correct value for each context method' do
16+
expect(proxy_context.clock_diff).must_equal 1681486457423
17+
expect(proxy_context.deadline_ms).must_equal 1681492072985
18+
expect(proxy_context.aws_request_id).must_equal "d6f5961b-5034-4db5-b3a9-fa378133b0f0"
19+
end
20+
21+
it 'should raise an error for unknown methods' do
22+
expect { proxy_context.foo }.must_raise NoMethodError
23+
end
24+
25+
it 'should return false for respond_to? for a unknown method' do
26+
expect(proxy_context.respond_to?(:foo)).must_equal false
27+
end
28+
29+
end

test/proxy_server_test.rb

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require 'net/http'
2+
require 'test_helper'
3+
4+
class ProxyServerTest < LambySpec
5+
let(:event) { TestHelpers::Events::HttpV2.create }
6+
let(:context) { TestHelpers::LambdaContext.raw_data }
7+
let(:rack_app) { Rack::Builder.new { run lambda { |env| [200, {}, StringIO.new('{"statusCode": 200}')] } }.to_app }
8+
let(:proxy) { Lamby::ProxyServer.new }
9+
10+
before { Lamby.config.rack_app = rack_app }
11+
12+
it 'should return a 405 helpful message on GET' do
13+
response = proxy.call(env("REQUEST_METHOD" => 'GET'))
14+
expect(response[:statusCode]).must_equal 405
15+
expect(response[:headers]).must_equal({"Content-Type" => "text/html"})
16+
expect(response[:body]).must_include 'Method Not Allowed'
17+
end
18+
19+
it 'should call Lamby.cmd on POST' do
20+
response = proxy.call(env)
21+
expect(response[:statusCode]).must_equal 200
22+
expect(response[:headers]).must_equal({})
23+
expect(response[:body]).must_equal '{"statusCode": 200}'
24+
end
25+
26+
private
27+
28+
def env(options={})
29+
json = {"event": event, "context": context}.to_json
30+
{ 'REQUEST_METHOD' => 'POST',
31+
'PATH_INFO' => '/',
32+
'QUERY_STRING' => '',
33+
'SERVER_NAME' => 'localhost',
34+
'SERVER_PORT' => '3000',
35+
'HTTP_VERSION' => 'HTTP/1.1',
36+
'rack.version' => Rack::VERSION,
37+
'rack.input' => StringIO.new(json),
38+
'rack.url_scheme' => 'http',
39+
'rack.errors' => $stderr,
40+
'rack.multithread' => true,
41+
'rack.multiprocess' => false,
42+
'rack.run_once' => false,
43+
'CONTENT_TYPE' => 'application/json',
44+
'CONTENT_LENGTH' => json.bytesize.to_s
45+
}.merge(options)
46+
end
47+
end

test/test_helper/lambda_context.rb

+16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
module TestHelpers
22
class LambdaContext
33

4+
RAW_DATA ={
5+
"clock_diff" => 1681486457423,
6+
"deadline_ms" => 1681492072985,
7+
"aws_request_id" => "d6f5961b-5034-4db5-b3a9-fa378133b0f0",
8+
"invoked_function_arn" => "arn:aws:lambda:us-east-1:576043675419:function:lamby-ws-production-WSConnectLambda-5in18cNskwz6",
9+
"log_group_name" => "/aws/lambda/lamby-ws-production-WSConnectLambda-5in18cNskwz6",
10+
"log_stream_name" => "2023/04/14/[$LATEST]55a1d458479a4546b64acca17af3a69f",
11+
"function_name" => "lamby-ws-production-WSConnectLambda-5in18cNskwz6",
12+
"memory_limit_in_mb" => "1792",
13+
"function_version" => "$LATEST"
14+
}.freeze
15+
16+
def self.raw_data
17+
RAW_DATA.dup
18+
end
19+
420
def clock_diff
521
1585237646907
622
end

0 commit comments

Comments
 (0)