Skip to content

Commit b6c6dd7

Browse files
committed
Initial implementation
1 parent 614f872 commit b6c6dd7

File tree

6 files changed

+825
-1
lines changed

6 files changed

+825
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Crowbar
1+
![Logo](logo.svg)
22

33
TODO: Write a description here
44

logo.svg

Lines changed: 143 additions & 0 deletions
Loading

spec/crowbar_spec.cr

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
require "./spec_helper"
2+
3+
describe Crowbar do
4+
describe "handle_events buffered overload" do
5+
it "deserializes the events according to the event type" do
6+
sent_event = %({"result": 10})
7+
8+
LambdaTestServer.test_invocation(sent_event, String) do |received_event, _|
9+
received_event.should eq sent_event
10+
end
11+
12+
LambdaTestServer.test_invocation(sent_event, Bytes) do |received_event, _|
13+
received_event.should eq sent_event.to_slice
14+
end
15+
16+
LambdaTestServer.test_invocation(sent_event, IO) do |received_event, _|
17+
received_event.gets_to_end.should eq sent_event
18+
end
19+
20+
LambdaTestServer.test_invocation(sent_event, NamedTuple(result: Int32)) do |received_event, _|
21+
received_event.should eq({result: 10})
22+
end
23+
end
24+
25+
it "properly generates the context object" do
26+
LambdaTestServer.test_invocation("", String) do |_, context|
27+
UUID.parse?(context.aws_request_id).should_not be_nil
28+
context.client_context.should be_nil
29+
context.deadline.should eq Time.utc(2023, 11, 14, 22, 13, 20)
30+
context.function_name.should eq "function_name"
31+
context.function_version.should eq "function_version"
32+
context.identity.should be_nil
33+
context.invoked_function_arn.should eq "arn:aws:lambda:region:account-id:function:test-function"
34+
context.log_group_name.should eq "log_group_name"
35+
context.log_stream_name.should eq "log_stream_name"
36+
context.memory_limit_in_mb.should eq 512
37+
end
38+
end
39+
40+
it "serializes the handler responses according to their types" do
41+
handler = ->(event : String, context : Crowbar::Context) { "String Response" }
42+
LambdaTestServer.test_invocation("", handler) do |context|
43+
context.request.body.not_nil!.gets_to_end.should eq "String Response"
44+
end
45+
46+
handler = ->(event : Bytes, context : Crowbar::Context) { Bytes[0, 0, 0, 0] }
47+
LambdaTestServer.test_invocation("", handler) do |context|
48+
context.request.body.not_nil!.getb_to_end.should eq Bytes[0, 0, 0, 0]
49+
end
50+
51+
handler = ->(event : Bytes, context : Crowbar::Context) { IO::Memory.new "{}" }
52+
LambdaTestServer.test_invocation("", handler) do |context|
53+
context.request.body.not_nil!.gets_to_end.should eq "{}"
54+
end
55+
56+
handler = ->(event : Bytes, context : Crowbar::Context) { {status_code: 200} }
57+
LambdaTestServer.test_invocation("", handler) do |context|
58+
context.request.body.not_nil!.gets_to_end.should eq %({"status_code":200})
59+
end
60+
end
61+
62+
it "handles errors properly by posting to error endpoint" do
63+
event = %({"wrong_key": 10})
64+
65+
Crowbar.capture_log_output do
66+
handler = ->(event : String, context : Crowbar::Context) { raise "Error on handler" }
67+
LambdaTestServer.test_invocation(event, handler) do |context|
68+
context.request.path.should end_with "/error"
69+
error = LambdaTestServer::HandlerError.from_json(context.request.body.not_nil!)
70+
error.message.should eq "Error on handler"
71+
error.type.should eq "Exception"
72+
error.stack_trace.first.should match /spec\/crowbar_spec.cr:\d+:\d+ in '->'/
73+
end
74+
75+
handler = ->(event : NamedTuple(value: Int32), context : Crowbar::Context) { event[:value] }
76+
LambdaTestServer.test_invocation(event, handler) do |context|
77+
context.request.path.should end_with "/error"
78+
error = LambdaTestServer::HandlerError.from_json(context.request.body.not_nil!)
79+
error.message.should eq "Missing json attribute: value at line 1, column 1"
80+
error.type.should eq "JSON::ParseException"
81+
error.stack_trace.first.should match /\/usr\/lib\/crystal\/json\/from_json.cr:\d+:\d+ in 'new'/
82+
end
83+
end
84+
end
85+
end
86+
87+
describe "handle_events streaming overload" do
88+
it "deserializes the events according to the event type" do
89+
sent_event = %({"result": 10})
90+
91+
LambdaTestServer.test_invocation(sent_event, String, Crowbar::ResponseIO) do |received_event, _, _|
92+
received_event.should eq sent_event
93+
end
94+
95+
LambdaTestServer.test_invocation(sent_event, Bytes, Crowbar::ResponseIO) do |received_event, _, _|
96+
received_event.should eq sent_event.to_slice
97+
end
98+
99+
LambdaTestServer.test_invocation(sent_event, IO, Crowbar::ResponseIO) do |received_event, _, _|
100+
received_event.gets_to_end.should eq sent_event
101+
end
102+
103+
LambdaTestServer.test_invocation(sent_event, NamedTuple(result: Int32), Crowbar::ResponseIO) do |received_event, _, _|
104+
received_event.should eq({result: 10})
105+
end
106+
end
107+
108+
it "properly generates the context object" do
109+
LambdaTestServer.test_invocation("", String, Crowbar::ResponseIO) do |_, context|
110+
UUID.parse?(context.aws_request_id).should_not be_nil
111+
context.client_context.should be_nil
112+
context.deadline.should eq Time.utc(2023, 11, 14, 22, 13, 20)
113+
context.function_name.should eq "function_name"
114+
context.function_version.should eq "function_version"
115+
context.identity.should be_nil
116+
context.invoked_function_arn.should eq "arn:aws:lambda:region:account-id:function:test-function"
117+
context.log_group_name.should eq "log_group_name"
118+
context.log_stream_name.should eq "log_stream_name"
119+
context.memory_limit_in_mb.should eq 512
120+
end
121+
end
122+
123+
it "progressively streams the handler response" do
124+
handler = ->(event : String, context : Crowbar::Context, io : Crowbar::ResponseIO) do
125+
io << "First line of content"
126+
io.flush
127+
io << "Second line of content"
128+
end
129+
130+
LambdaTestServer.test_invocation("", handler) do |context|
131+
context.request.headers.should eq HTTP::Headers{
132+
"Content-Type" => "application/octet-stream",
133+
"Host" => "127.0.0.1:9876",
134+
"Lambda-Runtime-Function-Response-Mode" => "streaming",
135+
"Trailer" => "Lambda-Runtime-Function-Error-Type, Lambda-Runtime-Function-Error-Body",
136+
"Transfer-Encoding" => "chunked",
137+
}
138+
139+
raw_body = context.request.body.as(HTTP::ChunkedContent).io
140+
raw_body.gets(chomp: false).should eq "15\r\n"
141+
raw_body.gets(chomp: false).should eq "First line of content\r\n"
142+
raw_body.gets(chomp: false).should eq "16\r\n"
143+
raw_body.gets(chomp: false).should eq "Second line of content\r\n"
144+
raw_body.gets(chomp: false).should eq "0\r\n"
145+
raw_body.gets(chomp: false).should eq "\r\n"
146+
end
147+
end
148+
149+
it "handles errors before writes to the response io by posting to error endpoint" do
150+
event = %({"wrong_key": 10})
151+
152+
Crowbar.capture_log_output do
153+
handler = ->(event : String, context : Crowbar::Context, io : Crowbar::ResponseIO) { raise "Error on handler" }
154+
LambdaTestServer.test_invocation(event, handler) do |context|
155+
context.request.path.should end_with "/error"
156+
error = LambdaTestServer::HandlerError.from_json(context.request.body.not_nil!)
157+
error.message.should eq "Error on handler"
158+
error.type.should eq "Exception"
159+
error.stack_trace.first.should match /spec\/crowbar_spec.cr:\d+:\d+ in '->'/
160+
end
161+
162+
handler = ->(event : NamedTuple(value: Int32), context : Crowbar::Context, io : Crowbar::ResponseIO) { event[:value] }
163+
LambdaTestServer.test_invocation(event, handler) do |context|
164+
context.request.path.should end_with "/error"
165+
error = LambdaTestServer::HandlerError.from_json(context.request.body.not_nil!)
166+
error.message.should eq "Missing json attribute: value at line 1, column 1"
167+
error.type.should eq "JSON::ParseException"
168+
error.stack_trace.first.should match /\/usr\/lib\/crystal\/json\/from_json.cr:\d+:\d+ in 'new'/
169+
end
170+
end
171+
end
172+
173+
it "handles errors after writes to the response io by sending trailer" do
174+
event = %({"wrong_key": 10})
175+
176+
handler = ->(event : String, context : Crowbar::Context, io : Crowbar::ResponseIO) {
177+
io << "Initial write"
178+
io.flush
179+
raise "Error on handler"
180+
}
181+
182+
Crowbar.capture_log_output do
183+
LambdaTestServer.test_invocation(event, handler) do |context|
184+
context.request.path.should end_with "/response"
185+
raw_body = context.request.body.as(HTTP::ChunkedContent).io
186+
raw_body.gets(chomp: false).should eq "d\r\n"
187+
raw_body.gets(chomp: false).should eq "Initial write\r\n"
188+
raw_body.gets(chomp: false).should eq "0\r\n"
189+
raw_body.gets(chomp: false).should eq "Lambda-Runtime-Function-Error-Type: Exception\r\n"
190+
raw_body.gets(chomp: false).should match /Lambda-Runtime-Function-Error-Body: .+\r\n/
191+
raw_body.gets(chomp: false).should eq "\r\n"
192+
end
193+
end
194+
end
195+
196+
it "serializes http metadata for HttpResponseIO" do
197+
handler = ->(event : String, context : Crowbar::Context, io : Crowbar::HttpResponseIO) do
198+
cookies = HTTP::Cookies{"flavor" => "chocolate", "topper" => "cream"}
199+
cookies["topper"].expires = Time.utc(2025, 1, 1, 10, 10, 10)
200+
201+
io.headers = HTTP::Headers{"Content-Type" => "application/json", "Target" => "Mars"}
202+
io.cookies = cookies
203+
204+
io << %({"key":)
205+
io.flush
206+
io << %( "value"})
207+
end
208+
209+
LambdaTestServer.test_invocation("", handler) do |context|
210+
context.request.headers.should eq HTTP::Headers{
211+
"Content-Type" => "application/vnd.awslambda.http-integration-response",
212+
"Host" => "127.0.0.1:9876",
213+
"Lambda-Runtime-Function-Response-Mode" => "streaming",
214+
"Trailer" => "Lambda-Runtime-Function-Error-Type, Lambda-Runtime-Function-Error-Body",
215+
"Transfer-Encoding" => "chunked",
216+
}
217+
218+
raw_body = context.request.body.as(HTTP::ChunkedContent).io
219+
raw_body.gets(chomp: false).should eq "ae\r\n"
220+
raw_body.gets(chomp: false).should eq %({"statusCode":"ok","headers":{"Content-Type":"application/json","Target":"Mars"},"cookies":["flavor=chocolate","topper=cream; expires=Wed, 01 Jan 2025 10:10:10 GMT"]}\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\r\n)
221+
raw_body.gets(chomp: false).should eq "7\r\n"
222+
raw_body.gets(chomp: false).should eq %({"key":\r\n)
223+
raw_body.gets(chomp: false).should eq "9\r\n"
224+
raw_body.gets(chomp: false).should eq %( "value"}\r\n)
225+
raw_body.gets(chomp: false).should eq "0\r\n"
226+
raw_body.gets(chomp: false).should eq "\r\n"
227+
end
228+
end
229+
end
230+
end

0 commit comments

Comments
 (0)