├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── bootstrap ├── console ├── setup ├── test └── update ├── images ├── social.png └── social2.png ├── lamby.gemspec ├── lib ├── lamby.rb └── lamby │ ├── cold_start_metrics.rb │ ├── config.rb │ ├── debug.rb │ ├── handler.rb │ ├── logger.rb │ ├── proxy_context.rb │ ├── proxy_server.rb │ ├── rack.rb │ ├── rack_alb.rb │ ├── rack_http.rb │ ├── rack_rest.rb │ ├── railtie.rb │ ├── ssm_parameter_store.rb │ ├── tasks.rake │ └── version.rb └── test ├── cold_start_metrics_test.rb ├── debug_test.rb ├── dummy_app ├── Rakefile ├── app │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ ├── .keep │ │ └── application_controller.rb │ ├── images │ │ └── 1.png │ ├── models │ │ └── dummy │ │ │ └── .keep │ └── views │ │ ├── application │ │ ├── index.html.erb │ │ └── percent.html.erb │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── rails │ └── rake ├── config.ru ├── config │ ├── boot.rb │ ├── initializers │ │ ├── .keep │ │ └── secret_token.rb │ └── routes.rb ├── init.rb └── public │ ├── .keep │ ├── 1-public.png │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt ├── handler_test.rb ├── lamby_core_test.rb ├── proxy_context_test.rb ├── proxy_server_test.rb ├── rack_alb_test.rb ├── rack_deflate_test.rb ├── rack_http_test.rb ├── rack_rest_test.rb ├── ssm_parameter_store_test.rb ├── test_helper.rb └── test_helper ├── dummy_app_helpers.rb ├── event_helpers.rb ├── events ├── alb.rb ├── base.rb ├── http_v1.rb ├── http_v1_post.rb ├── http_v2.rb ├── http_v2_post.rb ├── rest.rb └── rest_post.rb ├── jobs_helpers.rb ├── lambda_context.rb ├── lambdakiq_helpers.rb └── stream_helpers.rb /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/ruby:3.1 2 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lamby", 3 | "build": { 4 | "dockerfile": "Dockerfile" 5 | }, 6 | "features": { 7 | "ghcr.io/devcontainers/features/node:latest": {}, 8 | "ghcr.io/devcontainers/features/docker-in-docker:latest": {}, 9 | "ghcr.io/devcontainers/features/sshd:latest": {} 10 | }, 11 | "customizations": { 12 | "vscode": { 13 | "settings": { 14 | "editor.formatOnSave": true 15 | }, 16 | "extensions": [] 17 | } 18 | }, 19 | "remoteUser": "vscode" 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem 2 | on: 3 | workflow_dispatch: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | push: 9 | name: Push gem to RubyGems.org 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 14 | contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag 15 | 16 | steps: 17 | # Set up 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: true 23 | ruby-version: ruby 24 | 25 | # Release 26 | - uses: rubygems/release-gem@v1 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push] 3 | jobs: 4 | image: 5 | name: Image 6 | runs-on: ubuntu-20.04 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: docker/login-action@v2 10 | with: 11 | registry: ghcr.io 12 | username: ${{ github.repository_owner }} 13 | password: ${{ secrets.GITHUB_TOKEN }} 14 | - uses: devcontainers/ci@v0.3 15 | with: 16 | push: always 17 | imageName: ghcr.io/rails-lambda/lamby-devcontainer 18 | cacheFrom: ghcr.io/rails-lambda/lamby-devcontainer 19 | runCmd: echo DONE! 20 | test: 21 | runs-on: ubuntu-20.04 22 | needs: image 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Setup & Test 27 | uses: devcontainers/ci@v0.3 28 | with: 29 | push: never 30 | cacheFrom: ghcr.io/rails-lambda/lamby-devcontainer 31 | env: | 32 | CI 33 | runCmd: | 34 | ./bin/setup 35 | ./bin/test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /pkg/ 6 | /spec/reports/ 7 | /tmp/ 8 | /vendor/bundle 9 | test/dummy_app/.gitignore 10 | test/dummy_app/app.rb 11 | test/dummy_app/template.yaml 12 | test/dummy_app/bin/build 13 | test/dummy_app/bin/deploy 14 | test/dummy_app/tmp 15 | test/dummy_app/log/**/* 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Keep A Changelog! 2 | 3 | See this http://keepachangelog.com link for information on how we want this documented formatted. 4 | 5 | ## v6.0.0 6 | 7 | - Remove "cookies" header from rack response to conform to Lambda proxy integration requirements. 8 | 9 | ## v6.0.0 10 | 11 | ### Changed 12 | 13 | - ⚠️ Breaking Changes ⚠️ 14 | - Remove Rack v2 support. 15 | - Added Rack v3 support. 16 | 17 | ## v5.2.1 18 | 19 | - Rack 3.X compatibility, by removing uninitialized constants. 20 | - Exit gracefully when SIGTERM or SIGINT signal occurs. 21 | 22 | ## v5.2.0 23 | 24 | ### Fixed 25 | 26 | - Safely Pass Percent Symbols in Paths Fixes #170 27 | 28 | ## v5.1.0 29 | 30 | ### Added 31 | 32 | - New CloudWatch cold start metrics. Defaults to off. Enable with `config.cold_start_metrics = true`. 33 | 34 | ## v5.0.0 35 | 36 | ### Changed 37 | 38 | - ⚠️ Breaking Changes ⚠️ 39 | - Remove Lamby::Runner & Lamby::Command in favor of LambdaConsole Ruby gem. 40 | - New runner pattern exceptions from above. Now: LambdaConsole::Run::UnknownCommandPattern 41 | - Switch your own runner events to new https://github.com/rails-lambda/lambda-console spec. 42 | 43 | ## v4.3.1, v4.3.2 44 | 45 | ### Changed 46 | 47 | - Command response will be #inspect for every command. 48 | - Added x-lambda-console option to the event. 49 | 50 | ## v4.3.0 51 | 52 | ### Changed 53 | 54 | - Default Lamby::Runner::PATTERNS to allow everything. 55 | 56 | ### Added 57 | 58 | - New Lamby::Command for IRB style top level binding evals. 59 | 60 | ## v4.2.1 61 | 62 | ### Added 63 | 64 | - Local Development Proxy Server with Puma. See #164 65 | 66 | ## v4.2.0 67 | 68 | ### Added 69 | 70 | - Local Development Proxy Server. See #164 71 | 72 | ## v4.1.1 73 | 74 | ### Changed 75 | 76 | - New lamby.cloud site and GitHub community organization. 77 | 78 | ## v4.1.0 79 | 80 | ### Added 81 | 82 | - Future-ready LambdaCable.cmd handler detection. 83 | 84 | ## v4.0.2 85 | 86 | ### Fixed 87 | 88 | - Runner's Open3 uses crypteia friendly env. 89 | 90 | ## v4.0.1 91 | 92 | ### Added 93 | 94 | - New `Lamby.config.handled_proc` called with ensure via `Lamby.cmd` 95 | 96 | ## v4.0.0 97 | 98 | ### Added 99 | 100 | - New `Lamby.config.rack_app` with default Rack builder. 101 | - The `Lamby.cmd` to simplify `CMD` with the new config.app from above. 102 | 103 | #### Removed 104 | 105 | - All lamby installer templates. 106 | - Remove SAM env checks used during debug mode. 107 | - Removed showing environment variables in debug mode. 108 | - Need to `require: false` when adding the Lamby gem to your `Gemfile`. 109 | - Dotenv integration. Use [Crypteia](https://github.com/customink/crypteia) now. 110 | 111 | #### Changed 112 | 113 | - Tested Rack 3.x. 114 | 115 | ## v3.1.3 116 | 117 | #### Fixed 118 | 119 | - The ::Rack::Utils namespace. Fixes #123. 120 | 121 | ## v3.1.2 122 | 123 | #### Fixed 124 | 125 | - Lambdakiq Handler Integration. Fixes #120. 126 | 127 | ## v3.1.1 128 | 129 | #### Fixed 130 | 131 | - X-Request-Start header value for New Relic with API Gateway. 132 | 133 | ## v3.1.0 134 | 135 | #### Added 136 | 137 | - Add X-Request-Start header for New Relic with API Gateway. 138 | 139 | ## v3.0.3 140 | 141 | #### Fixed 142 | 143 | - Ruby 2.7 Warnings | Logger. Thanks @jessedoyle 144 | 145 | ## v3.0.2 146 | 147 | #### Added 148 | 149 | - Runner now returns STDOUT/STDERR as body. 150 | 151 | ## v3.0.1 152 | 153 | #### Fixed 154 | 155 | - Fix Lambdakiq integration. Thanks #97. 156 | 157 | ## v3.0.0 158 | 159 | #### Added 160 | 161 | - Automatically handle `Lambdakiq.jobs?(event)`. 162 | - New event for tasks like DB migrations. #80 #93 163 | 164 | #### Changed 165 | 166 | - Updated template files to latest lambda container standards. 167 | 168 | ## v2.8.0 169 | 170 | #### Fixed 171 | 172 | - Perform rack body closing hooks on request #85 173 | 174 | ## v2.7.1 175 | 176 | #### Removed 177 | 178 | - Bootsnap setup convenience require. 179 | 180 | ## v2.7.0 181 | 182 | #### Added 183 | 184 | - Support EventBridge events in handler with default proc to log. 185 | 186 | ## v2.6.3 187 | 188 | #### Added 189 | 190 | - Bootsnap setup convenience require. 191 | 192 | ## v2.6.2 193 | 194 | - Fixed Rack::Deflate usage with an ALB. 195 | 196 | ## v2.6.1 197 | 198 | #### Fixed 199 | 200 | - Support redirects with empty response body. 201 | 202 | #### Added 203 | 204 | - Tests for enabling Rack::Deflate middleware by passing RACK_DEFLATE_ENABLED env variable. 205 | 206 | ## v2.6.0 207 | 208 | #### Fixed 209 | 210 | - Support multiple Set-Cookie headers for all rest types. 211 | 212 | ## v2.5.3 213 | 214 | #### Fixed 215 | 216 | - Base64 encode response body if the rack response is gzip or brotli compressed. 217 | 218 | ## v2.5.2 219 | 220 | - SSM file always overwrites. Fixes #65. 221 | 222 | ## v2.5.1 223 | 224 | #### Fixed 225 | 226 | - Quoting in describe-subnets #62 Thanks @atwoodjw 227 | 228 | ## v2.5.0 229 | 230 | #### Changed 231 | 232 | - Install files to favor containers. 233 | 234 | ## v2.2.2 235 | 236 | #### Changed 237 | 238 | - More ActiveSupport removal. Better ENV.to_h. 239 | 240 | ## v2.2.1 241 | 242 | #### Changed 243 | 244 | - More ActiveSupport removal from SsmParameterStore. 245 | 246 | ## v2.2.0 247 | 248 | #### Changed 249 | 250 | - Remove dependency on `activesupport` for rack-only applications. 251 | - Remove ActiveSupport artifacts: 252 | - Replace `strip_heredoc` with `<<~HEREDOC`. 253 | - Remove instances of `Object#try`, replace with `&.`. 254 | - Use `Rack::Utils.build_nested_query` in place of `Object#to_query`. 255 | - Replace `Object#present?` with `to_s.empty?`. 256 | - Replace `Array.wrap` with `Array[obj].compact.flatten`. 257 | - Add a check against the `RAILS_ENV` AND `RACK_ENV` environment 258 | variables prior to enabling debug mode. 259 | 260 | ## v2.1.0 261 | 262 | #### Changed 263 | 264 | - Only load the railtie if `Rails` is defined. 265 | 266 | ## v2.0.1 267 | 268 | #### Changed 269 | 270 | - Remove Rails runtime dep. Only rack is needed. 271 | 272 | ## v2.0.0 273 | 274 | Support for new API Gateway HTTP APIs!!! 275 | 276 | #### Changed 277 | 278 | - The `Lamby.handler` must have a `:rack` option. One of `:http`, `:rest`, `:alb`. 279 | - Renamed template generators to match options above. 280 | - The `lamby:install` task now defaults to HTTP API. 281 | - Changed the name of `:api` rack option to `:rest`. 282 | - Removed `export` from Dotenv files. Better Docker compatability. 283 | 284 | #### Added 285 | 286 | - New rack handler for HTTP API v1 and v2. 287 | - Lots of backfill tests for, ALBs & REST APIs. 288 | 289 | ## v1.0.3 290 | 291 | #### Changed 292 | 293 | - Change shebangs to `#!/usr/bin/env bash` 294 | 295 | ## v1.0.2 296 | 297 | #### Changed 298 | 299 | - Adds an optional 'overwrite' parameter to #to_env. 300 | 301 | ## v1.0.1 302 | 303 | #### Changed 304 | 305 | - Links in bin/build templates to point to lamby.custominktech.com site. 306 | 307 | ## v1.0.0 308 | 309 | #### Fixed 310 | 311 | - ALB query params & binary responses. Fixes #38. 312 | 313 | ## v0.6.0 314 | 315 | #### Added 316 | 317 | - APPLICATION LOAD BALANACER SUPPORT!!! The new default. Use `rack: :api` option to handler for API Gateway support. 318 | 319 | #### Changed 320 | 321 | - Rake task `lamby:install` now defaults to `application_load_balancer` 322 | 323 | ## v0.5.1 324 | 325 | #### Fixed 326 | 327 | - The .gitignore file template. Fix .aws-sam dir. 328 | 329 | ## v0.5.0 330 | 331 | #### Added 332 | 333 | - Template generators for first install. Ex: `./bin/rake -r lamby lamby:install:api_gateway`. 334 | - New `Lamby::SsmParameterStore.get!` helper. 335 | 336 | ## v0.4.1 337 | 338 | #### Fixed 339 | 340 | - Fix type in v0.4.0 fix below. 341 | 342 | ## v0.4.0 343 | 344 | #### Fixed 345 | 346 | - File uploads in #33 using `CONTENT_TYPE` and `CONTENT_LENGTH`. 347 | 348 | ## v0.3.2 349 | 350 | #### Added 351 | 352 | - Pass Request ID for CloudWatch logs. Fixes #30. 353 | 354 | ## v0.3.1 355 | 356 | #### Changed 357 | 358 | - Docs and SAM template tweaks. 359 | 360 | ## v0.3.0 361 | 362 | #### Added 363 | 364 | - Secure configs rake task. 365 | - Project bin setup and tests. 366 | 367 | #### Changed 368 | 369 | - SAM template tweaks. 370 | 371 | ## v0.2.0 372 | 373 | #### Changed 374 | 375 | - Simple docs and project re-organization. 376 | 377 | ## v0.1.0 378 | 379 | #### Added 380 | 381 | - New gem and placeholder in rubygems. 382 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ken@metaskills.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 3 | gemspec 4 | 5 | gem 'rails', '7.1.3.4' 6 | gem 'mocha', '~> 2.4' 7 | gem 'rack', "~> 3.1", ">= 3.1.7" 8 | 9 | group :test do 10 | gem 'lambdakiq' 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | lamby (6.0.1) 5 | lambda-console-ruby 6 | rack (>= 3.0.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (7.1.3.4) 12 | actionpack (= 7.1.3.4) 13 | activesupport (= 7.1.3.4) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | zeitwerk (~> 2.6) 17 | actionmailbox (7.1.3.4) 18 | actionpack (= 7.1.3.4) 19 | activejob (= 7.1.3.4) 20 | activerecord (= 7.1.3.4) 21 | activestorage (= 7.1.3.4) 22 | activesupport (= 7.1.3.4) 23 | mail (>= 2.7.1) 24 | net-imap 25 | net-pop 26 | net-smtp 27 | actionmailer (7.1.3.4) 28 | actionpack (= 7.1.3.4) 29 | actionview (= 7.1.3.4) 30 | activejob (= 7.1.3.4) 31 | activesupport (= 7.1.3.4) 32 | mail (~> 2.5, >= 2.5.4) 33 | net-imap 34 | net-pop 35 | net-smtp 36 | rails-dom-testing (~> 2.2) 37 | actionpack (7.1.3.4) 38 | actionview (= 7.1.3.4) 39 | activesupport (= 7.1.3.4) 40 | nokogiri (>= 1.8.5) 41 | racc 42 | rack (>= 2.2.4) 43 | rack-session (>= 1.0.1) 44 | rack-test (>= 0.6.3) 45 | rails-dom-testing (~> 2.2) 46 | rails-html-sanitizer (~> 1.6) 47 | actiontext (7.1.3.4) 48 | actionpack (= 7.1.3.4) 49 | activerecord (= 7.1.3.4) 50 | activestorage (= 7.1.3.4) 51 | activesupport (= 7.1.3.4) 52 | globalid (>= 0.6.0) 53 | nokogiri (>= 1.8.5) 54 | actionview (7.1.3.4) 55 | activesupport (= 7.1.3.4) 56 | builder (~> 3.1) 57 | erubi (~> 1.11) 58 | rails-dom-testing (~> 2.2) 59 | rails-html-sanitizer (~> 1.6) 60 | activejob (7.1.3.4) 61 | activesupport (= 7.1.3.4) 62 | globalid (>= 0.3.6) 63 | activemodel (7.1.3.4) 64 | activesupport (= 7.1.3.4) 65 | activerecord (7.1.3.4) 66 | activemodel (= 7.1.3.4) 67 | activesupport (= 7.1.3.4) 68 | timeout (>= 0.4.0) 69 | activestorage (7.1.3.4) 70 | actionpack (= 7.1.3.4) 71 | activejob (= 7.1.3.4) 72 | activerecord (= 7.1.3.4) 73 | activesupport (= 7.1.3.4) 74 | marcel (~> 1.0) 75 | activesupport (7.1.3.4) 76 | base64 77 | bigdecimal 78 | concurrent-ruby (~> 1.0, >= 1.0.2) 79 | connection_pool (>= 2.2.5) 80 | drb 81 | i18n (>= 1.6, < 2) 82 | minitest (>= 5.1) 83 | mutex_m 84 | tzinfo (~> 2.0) 85 | aws-eventstream (1.3.0) 86 | aws-partitions (1.956.0) 87 | aws-sdk-core (3.201.1) 88 | aws-eventstream (~> 1, >= 1.3.0) 89 | aws-partitions (~> 1, >= 1.651.0) 90 | aws-sigv4 (~> 1.8) 91 | jmespath (~> 1, >= 1.6.1) 92 | aws-sdk-sqs (1.80.0) 93 | aws-sdk-core (~> 3, >= 3.201.0) 94 | aws-sigv4 (~> 1.5) 95 | aws-sdk-ssm (1.150.0) 96 | aws-sdk-core (~> 3, >= 3.165.0) 97 | aws-sigv4 (~> 1.1) 98 | aws-sigv4 (1.8.0) 99 | aws-eventstream (~> 1, >= 1.0.2) 100 | base64 (0.2.0) 101 | bigdecimal (3.1.8) 102 | builder (3.3.0) 103 | coderay (1.1.3) 104 | concurrent-ruby (1.3.3) 105 | connection_pool (2.4.1) 106 | crass (1.0.6) 107 | date (3.3.4) 108 | drb (2.2.1) 109 | erubi (1.13.0) 110 | globalid (1.2.1) 111 | activesupport (>= 6.1) 112 | i18n (1.14.5) 113 | concurrent-ruby (~> 1.0) 114 | io-console (0.7.2) 115 | irb (1.14.0) 116 | rdoc (>= 4.0.0) 117 | reline (>= 0.4.2) 118 | jmespath (1.6.2) 119 | lambda-console-ruby (1.0.0) 120 | lambdakiq (2.2.0) 121 | activejob 122 | aws-sdk-sqs 123 | concurrent-ruby 124 | railties 125 | loofah (2.22.0) 126 | crass (~> 1.0.2) 127 | nokogiri (>= 1.12.0) 128 | mail (2.8.1) 129 | mini_mime (>= 0.1.1) 130 | net-imap 131 | net-pop 132 | net-smtp 133 | marcel (1.0.4) 134 | method_source (1.0.0) 135 | mini_mime (1.1.5) 136 | minitest (5.24.1) 137 | minitest-focus (1.3.1) 138 | minitest (>= 4, < 6) 139 | mocha (2.4.0) 140 | ruby2_keywords (>= 0.0.5) 141 | mutex_m (0.2.0) 142 | net-imap (0.4.14) 143 | date 144 | net-protocol 145 | net-pop (0.1.2) 146 | net-protocol 147 | net-protocol (0.2.2) 148 | timeout 149 | net-smtp (0.5.0) 150 | net-protocol 151 | nio4r (2.7.3) 152 | nokogiri (1.16.6-aarch64-linux) 153 | racc (~> 1.4) 154 | nokogiri (1.16.6-arm64-darwin) 155 | racc (~> 1.4) 156 | nokogiri (1.16.6-x86_64-linux) 157 | racc (~> 1.4) 158 | pry (0.14.2) 159 | coderay (~> 1.1) 160 | method_source (~> 1.0) 161 | psych (5.1.2) 162 | stringio 163 | racc (1.8.0) 164 | rack (3.1.7) 165 | rack-session (2.0.0) 166 | rack (>= 3.0.0) 167 | rack-test (2.1.0) 168 | rack (>= 1.3) 169 | rackup (2.1.0) 170 | rack (>= 3) 171 | webrick (~> 1.8) 172 | rails (7.1.3.4) 173 | actioncable (= 7.1.3.4) 174 | actionmailbox (= 7.1.3.4) 175 | actionmailer (= 7.1.3.4) 176 | actionpack (= 7.1.3.4) 177 | actiontext (= 7.1.3.4) 178 | actionview (= 7.1.3.4) 179 | activejob (= 7.1.3.4) 180 | activemodel (= 7.1.3.4) 181 | activerecord (= 7.1.3.4) 182 | activestorage (= 7.1.3.4) 183 | activesupport (= 7.1.3.4) 184 | bundler (>= 1.15.0) 185 | railties (= 7.1.3.4) 186 | rails-dom-testing (2.2.0) 187 | activesupport (>= 5.0.0) 188 | minitest 189 | nokogiri (>= 1.6) 190 | rails-html-sanitizer (1.6.0) 191 | loofah (~> 2.21) 192 | nokogiri (~> 1.14) 193 | railties (7.1.3.4) 194 | actionpack (= 7.1.3.4) 195 | activesupport (= 7.1.3.4) 196 | irb 197 | rackup (>= 1.0.0) 198 | rake (>= 12.2) 199 | thor (~> 1.0, >= 1.2.2) 200 | zeitwerk (~> 2.6) 201 | rake (13.2.1) 202 | rdoc (6.7.0) 203 | psych (>= 4.0.0) 204 | reline (0.5.9) 205 | io-console (~> 0.5) 206 | ruby2_keywords (0.0.5) 207 | stringio (3.1.1) 208 | thor (1.3.1) 209 | timecop (0.9.6) 210 | timeout (0.4.1) 211 | tzinfo (2.0.6) 212 | concurrent-ruby (~> 1.0) 213 | webrick (1.8.1) 214 | websocket-driver (0.7.6) 215 | websocket-extensions (>= 0.1.0) 216 | websocket-extensions (0.1.5) 217 | zeitwerk (2.6.16) 218 | 219 | PLATFORMS 220 | aarch64-linux 221 | arm64-darwin-21 222 | arm64-darwin-22 223 | arm64-darwin-23 224 | x86_64-linux 225 | 226 | DEPENDENCIES 227 | aws-sdk-ssm 228 | bundler 229 | lambdakiq 230 | lamby! 231 | minitest 232 | minitest-focus 233 | mocha (~> 2.4) 234 | pry 235 | rack (~> 3.1, >= 3.1.7) 236 | rails (= 7.1.3.4) 237 | rake 238 | timecop 239 | webrick 240 | 241 | BUNDLED WITH 242 | 2.3.26 243 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ken Collins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lamby [![Actions Status](https://github.com/rails-lambda/lamby/workflows/CI/CD/badge.svg)](https://github.com/rails-lambda/lamby/actions) 2 | 3 |

Simple Rails & AWS Lambda Integration using Rack

4 | Lamby: Simple Rails & AWS Lambda Integration using Rack. 5 | 6 | Lamby is an [AWS Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter) for Rack applications. 7 | 8 | We support Lambda [Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html), [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) (HTTP or REST, all payload versions), and even [Application Load Balancer](https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html) integrations. 9 | 10 | ## Quick Start 11 | 12 | https://lamby.cloud/docs/quick-start 13 | 14 | ## Full Documentation 15 | 16 | https://lamby.cloud/docs/anatomy 17 | 18 | ## Contributing 19 | 20 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/rails-lambda/lamby) 21 | 22 | This project is built for [GitHub Codespaces](https://github.com/features/codespaces) using the [Development Container](https://containers.dev) specification. Once you have the repo cloned and setup with a dev container using either Codespaces or [VS Code](#using-vs-code), run the following commands. This will install packages and run tests. 23 | 24 | ```shell 25 | $ ./bin/setup 26 | $ ./bin/test 27 | ``` 28 | 29 | #### Using VS Code 30 | 31 | If you have the [Visual Studio Code Dev Container](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension installed you can easily clone this repo locally, use the "Open Folder in Container..." command. This allows you to use the integrated terminal for the commands above. 32 | 33 | ## Code of Conduct 34 | 35 | Everyone interacting in the Lamby project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rails-lambda/lamby/blob/master/CODE_OF_CONDUCT.md). 36 | 37 | Bug reports and pull requests are welcome on GitHub at https://github.com/rails-lambda/lamby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 38 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | ENV['RUBYOPT'] = '-W:no-deprecated -W:no-experimental' 2 | require "bundler/gem_tasks" 3 | require "rake/testtask" 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << "test" 7 | t.libs << "lib" 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 15 | t.verbose = false 16 | t.warning = false 17 | end 18 | 19 | Rake::TestTask.new(:test_deflate) do |t| 20 | t.libs << "test" 21 | t.libs << "lib" 22 | t.test_files = FileList["test/rack_deflate_test.rb"] 23 | t.verbose = false 24 | t.warning = false 25 | end 26 | 27 | task :default => [:test, :test_deflate] 28 | -------------------------------------------------------------------------------- /bin/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "lamby" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo '== Installing dependencies ==' 5 | bundle config set --local path 'vendor/bundle' 6 | bundle install -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export RAILS_ENV="test" 5 | 6 | bundle exec rake test 7 | LAMBY_TEST_DYNAMIC_HANDLER=1 bundle exec rake test 8 | LAMBY_RACK_DEFLATE_ENABLED=1 bundle exec rake test_deflate -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | rm -rf ./vendor/bundle 5 | ./bin/setup 6 | -------------------------------------------------------------------------------- /images/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/images/social.png -------------------------------------------------------------------------------- /images/social2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/images/social2.png -------------------------------------------------------------------------------- /lamby.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'lamby/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "lamby" 8 | spec.version = Lamby::VERSION 9 | spec.authors = ["Ken Collins"] 10 | spec.email = ["ken@metaskills.net"] 11 | spec.summary = %q{Simple Rails & AWS Lambda Integration using Rack} 12 | spec.description = %q{Simple Rails & AWS Lambda Integration using Rack and various utilities.} 13 | spec.homepage = "https://github.com/rails-lambda/lamby" 14 | spec.license = "MIT" 15 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 16 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | end 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | spec.add_dependency 'rack', '>= 3.0.0' 22 | spec.add_dependency 'lambda-console-ruby' 23 | spec.add_development_dependency 'aws-sdk-ssm' 24 | spec.add_development_dependency 'bundler' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'minitest' 27 | spec.add_development_dependency 'minitest-focus' 28 | spec.add_development_dependency 'mocha' 29 | spec.add_development_dependency 'pry' 30 | spec.add_development_dependency 'timecop' 31 | spec.add_development_dependency 'webrick' 32 | end 33 | -------------------------------------------------------------------------------- /lib/lamby.rb: -------------------------------------------------------------------------------- 1 | require 'lamby/logger' 2 | require 'rack' 3 | require 'base64' 4 | require 'lambda-console-ruby' 5 | require 'lamby/version' 6 | require 'lamby/config' 7 | require 'lamby/rack' 8 | require 'lamby/rack_alb' 9 | require 'lamby/rack_rest' 10 | require 'lamby/rack_http' 11 | require 'lamby/debug' 12 | require 'lamby/cold_start_metrics' 13 | require 'lamby/handler' 14 | 15 | if defined?(Rails) 16 | require 'rails/railtie' 17 | require 'lamby/railtie' 18 | end 19 | 20 | module Lamby 21 | 22 | extend self 23 | 24 | def cmd(event:, context:) 25 | handler(config.rack_app, event, context) 26 | ensure 27 | config.handled_proc.call(event, context) 28 | end 29 | 30 | def handler(app, event, context, options = {}) 31 | Handler.call(app, event, context, options) 32 | end 33 | 34 | def config 35 | Lamby::Config.config 36 | end 37 | 38 | autoload :SsmParameterStore, 'lamby/ssm_parameter_store' 39 | autoload :ProxyContext, 'lamby/proxy_context' 40 | autoload :ProxyServer, 'lamby/proxy_server' 41 | 42 | end 43 | 44 | # Add signal traps for clean exit 45 | Signal.trap("TERM") do 46 | puts "Received SIGTERM, exiting gracefully..." 47 | exit!(0) # exit! ensures no exception is raised 48 | end 49 | 50 | Signal.trap("INT") do 51 | puts "Received SIGINT, exiting gracefully..." 52 | exit!(0) # exit! ensures no exception is raised 53 | end -------------------------------------------------------------------------------- /lib/lamby/cold_start_metrics.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class ColdStartMetrics 3 | NAMESPACE = 'Lamby' 4 | 5 | @cold_start = true 6 | @cold_start_time = (Time.now.to_f * 1000).to_i 7 | 8 | class << self 9 | 10 | def instrument! 11 | return unless @cold_start 12 | @cold_start = false 13 | now = (Time.now.to_f * 1000).to_i 14 | proactive_init = (now - @cold_start_time) > 10_000 15 | new(proactive_init).instrument! 16 | end 17 | 18 | def clear! 19 | @cold_start = true 20 | @cold_start_time = (Time.now.to_f * 1000).to_i 21 | end 22 | 23 | end 24 | 25 | def initialize(proactive_init) 26 | @proactive_init = proactive_init 27 | @metrics = [] 28 | @properties = {} 29 | end 30 | 31 | def instrument! 32 | name = @proactive_init ? 'ProactiveInit' : 'ColdStart' 33 | put_metric name, 1, 'Count' 34 | puts JSON.dump(message) 35 | end 36 | 37 | private 38 | 39 | def dimensions 40 | [{ AppName: rails_app_name }] 41 | end 42 | 43 | def put_metric(name, value, unit = nil) 44 | @metrics << { 'Name': name }.tap do |m| 45 | m['Unit'] = unit if unit 46 | end 47 | set_property name, value 48 | end 49 | 50 | def set_property(name, value) 51 | @properties[name] = value 52 | self 53 | end 54 | 55 | def message 56 | { 57 | '_aws': { 58 | 'Timestamp': timestamp, 59 | 'CloudWatchMetrics': [ 60 | { 61 | 'Namespace': NAMESPACE, 62 | 'Dimensions': [dimensions.map(&:keys).flatten], 63 | 'Metrics': @metrics 64 | } 65 | ] 66 | } 67 | }.tap do |m| 68 | dimensions.each { |d| m.merge!(d) } 69 | m.merge!(@properties) 70 | end 71 | end 72 | 73 | def timestamp 74 | Time.current.strftime('%s%3N').to_i 75 | end 76 | 77 | def rails_app_name 78 | Lamby.config.metrics_app_name || 79 | Rails.application.class.name.split('::').first 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/lamby/config.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | module Config 3 | 4 | def configure 5 | yield(config) 6 | config 7 | end 8 | 9 | def reconfigure 10 | config.reconfigure { |c| yield(c) if block_given? } 11 | end 12 | 13 | def config 14 | @config ||= Configuration.new 15 | end 16 | 17 | extend self 18 | 19 | end 20 | 21 | class Configuration 22 | 23 | def initialize 24 | initialize_defaults 25 | end 26 | 27 | def reconfigure 28 | instance_variables.each { |var| instance_variable_set var, nil } 29 | initialize_defaults 30 | yield(self) if block_given? 31 | self 32 | end 33 | 34 | def rack_app 35 | @rack_app ||= ::Rack::Builder.new { run ::Rails.application }.to_app 36 | end 37 | 38 | def rack_app=(app) 39 | @rack_app = app 40 | end 41 | 42 | def initialize_defaults 43 | @rack_app = nil 44 | @cold_start_metrics = false 45 | @metrics_app_name = nil 46 | @event_bridge_handler = lambda { |event, context| puts(event) } 47 | end 48 | 49 | def event_bridge_handler 50 | @event_bridge_handler 51 | end 52 | 53 | def event_bridge_handler=(func) 54 | @event_bridge_handler = func 55 | end 56 | 57 | def runner_patterns 58 | LambdaConsole::Run::PATTERNS 59 | end 60 | 61 | def handled_proc 62 | @handled_proc ||= Proc.new { |_event, _context| } 63 | end 64 | 65 | def handled_proc=(proc) 66 | @handled_proc = proc 67 | end 68 | 69 | def cold_start_metrics? 70 | @cold_start_metrics 71 | end 72 | 73 | def cold_start_metrics=(bool) 74 | @cold_start_metrics = bool 75 | end 76 | 77 | def metrics_app_name 78 | @metrics_app_name 79 | end 80 | 81 | def metrics_app_name=(name) 82 | @metrics_app_name = name 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/lamby/debug.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | module Debug 3 | extend self 4 | 5 | def on?(event) 6 | params = event['multiValueQueryStringParameters'] || event['queryStringParameters'] 7 | (development? || ENV['LAMBY_DEBUG']) && params && params['debug'] == '1' 8 | end 9 | 10 | def call(event, context, env) 11 | [ 200, { 'Content-Type' => 'text/html' }, [body(event, context, env)] ] 12 | end 13 | 14 | private 15 | 16 | def body(event, context, env) 17 | <<-HTML 18 | 19 | 20 | 21 |

Lamby Debug Response

22 |

Event

23 |
24 |               #{JSON.pretty_generate(event)}
25 |             
26 |

Rack Env

27 |
28 |               #{JSON.pretty_generate(env)}
29 |             
30 |

#{context.class.name}

31 | 32 | #{CGI::escapeHTML(context.inspect)} 33 | 34 | 35 | 36 | HTML 37 | end 38 | 39 | def development? 40 | ENV.to_h 41 | .slice('RACK_ENV', 'RAILS_ENV') 42 | .values 43 | .any? { |v| v.to_s.casecmp('development').zero? } 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/lamby/handler.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class Handler 3 | 4 | class << self 5 | 6 | def call(app, event, context, options = {}) 7 | Lamby::ColdStartMetrics.instrument! if Lamby.config.cold_start_metrics? 8 | new(app, event, context, options).call.response 9 | end 10 | 11 | end 12 | 13 | def initialize(app, event, context, options = {}) 14 | @app = app 15 | @event = event 16 | @context = context 17 | @options = options 18 | end 19 | 20 | def response 21 | @response 22 | end 23 | 24 | def status 25 | @status 26 | end 27 | 28 | def headers 29 | @headers 30 | end 31 | 32 | def set_cookies 33 | return @set_cookies if defined?(@set_cookies) 34 | set_cookie = @headers.delete("Set-Cookie") || @headers.delete("set-cookie") 35 | @set_cookies = if @headers && set_cookie 36 | Array(set_cookie).flat_map { |cookie| cookie.split("\n").map(&:strip) } 37 | end 38 | end 39 | 40 | def body 41 | @rbody ||= ''.tap do |rbody| 42 | @body.each { |part| rbody << part.to_s if part } 43 | @body.close if @body.respond_to? :close 44 | end 45 | end 46 | 47 | def call 48 | @response ||= call_app 49 | self 50 | end 51 | 52 | def base64_encodeable?(hdrs = @headers) 53 | hdrs && ( 54 | hdrs['content-transfer-encoding'] == 'binary' || 55 | hdrs['Content-Transfer-Encoding'] == 'binary' || 56 | content_encoding_compressed?(hdrs) || 57 | hdrs['X-Lamby-Base64'] == '1' 58 | ) 59 | end 60 | 61 | def body64 62 | Base64.strict_encode64(body) 63 | end 64 | 65 | private 66 | 67 | def rack 68 | return @rack if defined?(@rack) 69 | @rack = begin 70 | type = rack_option 71 | klass = Lamby::Rack.lookup type, @event 72 | (klass && klass.handle?(@event)) ? klass.new(@event, @context) : false 73 | end 74 | end 75 | 76 | def rack_option 77 | return if ENV['LAMBY_TEST_DYNAMIC_HANDLER'] 78 | @options[:rack] 79 | end 80 | 81 | def rack_response 82 | { statusCode: status, 83 | headers: stringify_values!(headers), 84 | body: body }.merge(rack.response(self)) 85 | end 86 | 87 | def stringify_values!(headers) 88 | headers.each do |k, v| 89 | headers[k] = v.to_s 90 | end 91 | headers 92 | end 93 | 94 | def call_app 95 | if Debug.on?(@event) 96 | Debug.call @event, @context, rack.env 97 | elsif rack? 98 | @status, @headers, @body = @app.call rack.env 99 | set_cookies 100 | rack_response 101 | elsif lambdakiq? 102 | Lambdakiq.cmd event: @event, context: @context 103 | elsif lambda_cable? 104 | LambdaCable.cmd event: @event, context: @context 105 | elsif LambdaConsole.handle?(@event) 106 | LambdaConsole.handle(@event) 107 | elsif event_bridge? 108 | Lamby.config.event_bridge_handler.call @event, @context 109 | else 110 | [404, {}, StringIO.new('')] 111 | end 112 | end 113 | 114 | def content_encoding_compressed?(hdrs) 115 | content_encoding_header = hdrs['Content-Encoding'] || hdrs['content-encoding'] || '' 116 | content_encoding_header.split(', ').any? { |h| ['br', 'gzip'].include?(h) } 117 | end 118 | 119 | def rack? 120 | rack 121 | end 122 | 123 | def event_bridge? 124 | Lamby.config.event_bridge_handler && 125 | @event.key?('source') && @event.key?('detail') && @event.key?('detail-type') 126 | end 127 | 128 | def lambdakiq? 129 | defined?(::Lambdakiq) && ::Lambdakiq.jobs?(@event) 130 | end 131 | 132 | def lambda_cable? 133 | defined?(::LambdaCable) && ::LambdaCable.handle?(@event, @context) 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/lamby/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | if ENV['AWS_EXECUTION_ENV'] 4 | ENV['RAILS_LOG_TO_STDOUT'] = '1' 5 | 6 | module Lamby 7 | module Logger 8 | 9 | def initialize(*args, **kwargs) 10 | args[0] = STDOUT 11 | super(*args, **kwargs) 12 | end 13 | 14 | end 15 | end 16 | 17 | Logger.prepend Lamby::Logger unless ENV['LAMBY_TEST'] 18 | end 19 | -------------------------------------------------------------------------------- /lib/lamby/proxy_context.rb: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /lib/lamby/proxy_server.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class ProxyServer 3 | 4 | METHOD_NOT_ALLOWED = <<-HEREDOC.strip 5 |

Method Not Allowed

6 |

Please POST to this endpoint with an application/json content type and JSON payload of your Lambda's event and context.

7 |

Example: { "event": event, "context": context }

8 | HEREDOC 9 | 10 | def call(env) 11 | return method_not_allowed unless method_allowed?(env) 12 | event, context = event_and_context(env) 13 | lambda_to_rack 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 | [405, {"Content-Type" => "text/html"}, [ METHOD_NOT_ALLOWED.dup ]] 30 | end 31 | 32 | def lambda_to_rack(response) 33 | [ 200, {"Content-Type" => "application/json"}, [ response.to_json ] ] 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/lamby/rack.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class Rack 3 | LAMBDA_EVENT = 'lambda.event'.freeze 4 | LAMBDA_CONTEXT = 'lambda.context'.freeze 5 | HTTP_X_REQUESTID = 'HTTP_X_REQUEST_ID'.freeze 6 | HTTP_X_REQUEST_START = 'HTTP_X_REQUEST_START'.freeze 7 | HTTP_COOKIE = 'HTTP_COOKIE'.freeze 8 | 9 | class << self 10 | 11 | def lookup(type, event) 12 | types[type] || types.values.detect { |t| t.handle?(event) } 13 | end 14 | 15 | # Order is important. REST is hardest to isolated with handle? method. 16 | def types 17 | { alb: RackAlb, 18 | http: RackHttp, 19 | rest: RackRest, 20 | api: RackRest } 21 | end 22 | 23 | end 24 | 25 | attr_reader :event, :context 26 | 27 | def initialize(event, context) 28 | @event = event 29 | @context = context 30 | end 31 | 32 | def env 33 | @env ||= env_base.merge!(env_headers) 34 | end 35 | 36 | def response(_handler) 37 | {} 38 | end 39 | 40 | def multi_value? 41 | false 42 | end 43 | 44 | private 45 | 46 | def env_base 47 | raise NotImplementedError 48 | end 49 | 50 | def env_headers 51 | headers.transform_keys do |key| 52 | "HTTP_#{key.to_s.upcase.tr '-', '_'}" 53 | end.tap do |hdrs| 54 | hdrs[HTTP_X_REQUESTID] = request_id 55 | hdrs[HTTP_X_REQUEST_START] = "t=#{request_start}" if request_start 56 | end 57 | end 58 | 59 | def content_type 60 | headers.delete('Content-Type') || headers.delete('content-type') || headers.delete('CONTENT_TYPE') 61 | end 62 | 63 | def content_length 64 | bytesize = body.bytesize.to_s if body 65 | headers.delete('Content-Length') || headers.delete('content-length') || headers.delete('CONTENT_LENGTH') || bytesize 66 | end 67 | 68 | def body 69 | @body ||= if event['body'] && base64_encoded? 70 | Base64.decode64 event['body'] 71 | else 72 | event['body'] 73 | end 74 | end 75 | 76 | def headers 77 | @headers ||= event['headers'] || {} 78 | end 79 | 80 | def query_string 81 | @query_string ||= if event.key?('rawQueryString') 82 | event['rawQueryString'] 83 | elsif event.key?('multiValueQueryStringParameters') 84 | query = event['multiValueQueryStringParameters'] || {} 85 | query.map do |key, value| 86 | value.map{ |v| "#{key}=#{v}" }.join('&') 87 | end.flatten.join('&') 88 | else 89 | build_query_string 90 | end 91 | end 92 | 93 | def build_query_string 94 | return if event['queryStringParameters'].nil? 95 | ::Rack::Utils.build_nested_query( 96 | event.fetch('queryStringParameters') 97 | ).gsub('[', '%5B') 98 | .gsub(']', '%5D') 99 | end 100 | 101 | def base64_encoded? 102 | event['isBase64Encoded'] 103 | end 104 | 105 | def request_id 106 | context.aws_request_id 107 | end 108 | 109 | def request_start 110 | event.dig('requestContext', 'timeEpoch') || 111 | event.dig('requestContext', 'requestTimeEpoch') 112 | end 113 | 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/lamby/rack_alb.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class RackAlb < Lamby::Rack 3 | 4 | class << self 5 | 6 | def handle?(event) 7 | event.key?('httpMethod') && 8 | event.dig('requestContext', 'elb') 9 | end 10 | 11 | end 12 | 13 | def alb? 14 | true 15 | end 16 | 17 | def multi_value? 18 | event.key? 'multiValueHeaders' 19 | end 20 | 21 | def response(handler) 22 | hhdrs = handler.headers 23 | if multi_value? 24 | multivalue_headers = hhdrs.transform_values { |v| Array[v].compact.flatten } 25 | multivalue_headers['Set-Cookie'] = handler.set_cookies if handler.set_cookies 26 | end 27 | status_description = "#{handler.status} #{::Rack::Utils::HTTP_STATUS_CODES[handler.status]}" 28 | base64_encode = handler.base64_encodeable?(hhdrs) 29 | body = Base64.strict_encode64(handler.body) if base64_encode 30 | { multiValueHeaders: multivalue_headers, 31 | statusDescription: status_description, 32 | isBase64Encoded: base64_encode, 33 | body: body }.compact 34 | end 35 | 36 | private 37 | 38 | def env_base 39 | rack_version = defined?(::Rack::VERSION) ? ::Rack::VERSION : ::Rack.release 40 | { ::Rack::REQUEST_METHOD => event['httpMethod'], 41 | ::Rack::SCRIPT_NAME => '', 42 | ::Rack::PATH_INFO => event['path'] || '', 43 | ::Rack::QUERY_STRING => query_string, 44 | ::Rack::SERVER_NAME => headers['host'], 45 | ::Rack::SERVER_PORT => headers['x-forwarded-port'], 46 | ::Rack::SERVER_PROTOCOL => 'HTTP/1.1', 47 | ::Rack::RACK_VERSION => rack_version, 48 | ::Rack::RACK_URL_SCHEME => headers['x-forwarded-proto'], 49 | ::Rack::RACK_INPUT => StringIO.new(body || ''), 50 | ::Rack::RACK_ERRORS => $stderr, 51 | LAMBDA_EVENT => event, 52 | LAMBDA_CONTEXT => context 53 | }.tap do |env| 54 | ct = content_type 55 | cl = content_length 56 | env['CONTENT_TYPE'] = ct if ct 57 | env['CONTENT_LENGTH'] = cl if cl 58 | end 59 | end 60 | 61 | def headers 62 | @headers ||= multi_value? ? headers_multi : super 63 | end 64 | 65 | def headers_multi 66 | Hash[(event['multiValueHeaders'] || {}).map do |k,v| 67 | if v.is_a?(Array) 68 | if k == 'x-forwarded-for' 69 | [k, v.join(', ')] 70 | else 71 | [k, v.first] 72 | end 73 | else 74 | [k,v] 75 | end 76 | end] 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/lamby/rack_http.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class RackHttp < Lamby::Rack 3 | 4 | class << self 5 | 6 | def handle?(event) 7 | event.key?('version') && 8 | ( event.dig('requestContext', 'http') || 9 | event.dig('requestContext', 'httpMethod') ) 10 | end 11 | 12 | end 13 | 14 | def response(handler) 15 | if handler.base64_encodeable? 16 | { isBase64Encoded: true, body: handler.body64 } 17 | else 18 | super 19 | end.tap do |r| 20 | if cookies = handler.set_cookies 21 | if payload_version_one? 22 | r[:multiValueHeaders] ||= {} 23 | r[:multiValueHeaders]['Set-Cookie'] = cookies 24 | else 25 | r[:cookies] = cookies 26 | end 27 | end 28 | end 29 | end 30 | 31 | private 32 | 33 | def env_base 34 | rack_version = defined?(::Rack::VERSION) ? ::Rack::VERSION : ::Rack.release 35 | { ::Rack::REQUEST_METHOD => request_method, 36 | ::Rack::SCRIPT_NAME => '', 37 | ::Rack::PATH_INFO => path_info, 38 | ::Rack::QUERY_STRING => query_string, 39 | ::Rack::SERVER_NAME => server_name, 40 | ::Rack::SERVER_PORT => server_port, 41 | ::Rack::SERVER_PROTOCOL => server_protocol, 42 | ::Rack::RACK_VERSION => rack_version, 43 | ::Rack::RACK_URL_SCHEME => 'https', 44 | ::Rack::RACK_INPUT => StringIO.new(body || ''), 45 | ::Rack::RACK_ERRORS => $stderr, 46 | LAMBDA_EVENT => event, 47 | LAMBDA_CONTEXT => context 48 | }.tap do |env| 49 | ct = content_type 50 | cl = content_length 51 | env['CONTENT_TYPE'] = ct if ct 52 | env['CONTENT_LENGTH'] = cl if cl 53 | end 54 | end 55 | 56 | def env_headers 57 | super.tap do |hdrs| 58 | if cookies.any? 59 | hdrs[HTTP_COOKIE] = cookies.join('; ') 60 | end 61 | end 62 | end 63 | 64 | def request_method 65 | event.dig('requestContext', 'http', 'method') || event['httpMethod'] 66 | end 67 | 68 | def cookies 69 | event['cookies'] || [] 70 | end 71 | 72 | # Using custom domain names with v1.0 yields a good `path` parameter sans 73 | # stage. However, v2.0 and others do not. So we are just going to remove stage 74 | # no matter waht from other places for both. 75 | # 76 | def path_info 77 | stage = event.dig('requestContext', 'stage') 78 | spath = event.dig('requestContext', 'http', 'path') || event.dig('requestContext', 'path') 79 | spath = event['rawPath'] if spath != event['rawPath'] && !payload_version_one? 80 | spath.sub /\A\/#{stage}/, '' 81 | end 82 | 83 | def server_name 84 | headers['x-forwarded-host'] || 85 | headers['X-Forwarded-Host'] || 86 | headers['host'] || 87 | headers['Host'] 88 | end 89 | 90 | def server_port 91 | headers['x-forwarded-port'] || headers['X-Forwarded-Port'] 92 | end 93 | 94 | def server_protocol 95 | event.dig('requestContext', 'http', 'protocol') || 96 | event.dig('requestContext', 'protocol') || 97 | 'HTTP/1.1' 98 | end 99 | 100 | def payload_version_one? 101 | event['version'] == '1.0' 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/lamby/rack_rest.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class RackRest < Lamby::Rack 3 | 4 | class << self 5 | 6 | def handle?(event) 7 | event.key?('httpMethod') 8 | end 9 | 10 | end 11 | 12 | def response(handler) 13 | if handler.base64_encodeable? 14 | { isBase64Encoded: true, body: handler.body64 } 15 | else 16 | super 17 | end.tap do |r| 18 | if cookies = handler.set_cookies 19 | r[:multiValueHeaders] ||= {} 20 | r[:multiValueHeaders]['Set-Cookie'] = cookies 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def env_base 28 | rack_version = defined?(::Rack::VERSION) ? ::Rack::VERSION : ::Rack.release 29 | { ::Rack::REQUEST_METHOD => event['httpMethod'], 30 | ::Rack::SCRIPT_NAME => '', 31 | ::Rack::PATH_INFO => event['path'] || '', 32 | ::Rack::QUERY_STRING => query_string, 33 | ::Rack::SERVER_NAME => headers['Host'], 34 | ::Rack::SERVER_PORT => headers['X-Forwarded-Port'], 35 | ::Rack::SERVER_PROTOCOL => event.dig('requestContext', 'protocol') || 'HTTP/1.1', 36 | ::Rack::RACK_VERSION => rack_version, 37 | ::Rack::RACK_URL_SCHEME => 'https', 38 | ::Rack::RACK_INPUT => StringIO.new(body || ''), 39 | ::Rack::RACK_ERRORS => $stderr, 40 | LAMBDA_EVENT => event, 41 | LAMBDA_CONTEXT => context 42 | }.tap do |env| 43 | ct = content_type 44 | cl = content_length 45 | env['CONTENT_TYPE'] = ct if ct 46 | env['CONTENT_LENGTH'] = cl if cl 47 | end 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/lamby/railtie.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | class Railtie < ::Rails::Railtie 3 | config.lamby = Lamby::Config.config 4 | 5 | rake_tasks do 6 | load 'lamby/tasks.rake' 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lamby/ssm_parameter_store.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ssm' 2 | 3 | module Lamby 4 | class SsmParameterStore 5 | 6 | MAX_RESULTS = 10 7 | 8 | Param = Struct.new :name, :env, :value 9 | 10 | attr_reader :path, :params 11 | 12 | class << self 13 | 14 | def dotenv(path) 15 | new(path).get!.to_dotenv 16 | end 17 | 18 | def get!(path) 19 | parts = path[1..-1].split('/') 20 | env = parts.pop 21 | path = "/#{parts.join('/')}" 22 | param = new(path).get!.params.detect do |p| 23 | p.env == env 24 | end 25 | param&.value 26 | end 27 | 28 | end 29 | 30 | def initialize(path, options = {}) 31 | @path = path 32 | @params = [] 33 | @options = options 34 | end 35 | 36 | def to_env(overwrite: true) 37 | params.each do |param| 38 | overwrite ? ENV[param.env] = param.value : ENV[param.env] ||= param.value 39 | end 40 | end 41 | 42 | def to_dotenv 43 | File.open(dotenv_file, 'a') { |f| f.write(dotenv_contents) } 44 | end 45 | 46 | def get! 47 | get_all! 48 | get_history! unless label.to_s.empty? 49 | self 50 | end 51 | 52 | def label 53 | ENV['LAMBY_SSM_PARAMS_LABEL'] || @options[:label] 54 | end 55 | 56 | def client 57 | @client ||= begin 58 | options = @options[:client_options] || {} 59 | Aws::SSM::Client.new(options) 60 | end 61 | end 62 | 63 | 64 | private 65 | 66 | def dotenv_file 67 | @options[:dotenv_file] || ENV['LAMBY_SSM_PARAMS_FILE'] || Rails.root.join(".env.#{Rails.env}") 68 | end 69 | 70 | def dotenv_contents 71 | params.each_with_object('') do |param, contents| 72 | line = "#{param.env}=#{param.value}\n" 73 | contents << line 74 | end 75 | end 76 | 77 | # Path 78 | 79 | def get_all! 80 | return params if @got_all 81 | get_parse_all 82 | while @all_response.next_token do get_parse_all end 83 | @got_all = true 84 | params 85 | end 86 | 87 | def get_parse_all 88 | get_all 89 | parse_all 90 | end 91 | 92 | def get_all 93 | @all_response = client.get_parameters_by_path(get_all_options) 94 | end 95 | 96 | def get_all_options 97 | { path: path, 98 | recursive: true, 99 | with_decryption: true, 100 | max_results: MAX_RESULTS 101 | }.tap { |options| 102 | token = @all_response&.next_token 103 | options[:next_token] = token if token 104 | } 105 | end 106 | 107 | def parse_all 108 | @all_response.parameters.each do |p| 109 | env = p.name.split('/').last 110 | params << Param.new(p.name, env, p.value) 111 | end 112 | end 113 | 114 | # History 115 | 116 | def get_history! 117 | return params if @got_history 118 | params.each do |param| 119 | name = param.name 120 | get_parse_history(name) 121 | while @hist_response.next_token do get_parse_history(name) end 122 | end 123 | @got_history = true 124 | params 125 | end 126 | 127 | def get_parse_history(name) 128 | get_history(name) 129 | parse_history(name) 130 | end 131 | 132 | def get_history(name) 133 | @hist_response = client.get_parameter_history(get_history_options(name)) 134 | end 135 | 136 | def get_history_options(name) 137 | { name: name, 138 | with_decryption: true, 139 | max_results: MAX_RESULTS 140 | }.tap { |options| 141 | token = @hist_response&.next_token 142 | options[:next_token] = token if token 143 | } 144 | end 145 | 146 | def parse_history(name) 147 | @hist_response.parameters.each do |p| 148 | next unless p.labels.include? label 149 | param = params.detect { |param| param.name == name } 150 | param.value = p.value 151 | end 152 | end 153 | 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/lamby/tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :lamby do 2 | task :proxy_server => [:environment] do 3 | require 'webrick' 4 | port = ENV['LAMBY_PROXY_PORT'] || 3000 5 | bind = ENV['LAMBY_PROXY_BIND'] || '0.0.0.0' 6 | Rack::Handler::WEBrick.run Lamby::ProxyServer.new, Port: port, BindAddress: bind 7 | end 8 | 9 | task :proxy_server_puma => [:environment] do 10 | port = ENV['LAMBY_PROXY_PORT'] || 3000 11 | host = ENV['LAMBY_PROXY_BIND'] || '0.0.0.0' 12 | lamby_proxy = Lamby::ProxyServer.new 13 | maybe_later = MaybeLater::Middleware.new(lamby_proxy) 14 | server = Puma::Server.new(maybe_later) 15 | server.add_tcp_listener host, port 16 | puts "Starting Puma server on #{host}:#{port}..." 17 | server.run.join 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lamby/version.rb: -------------------------------------------------------------------------------- 1 | module Lamby 2 | VERSION = '6.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /test/cold_start_metrics_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ColdStartMetricsSpec < LambySpec 4 | 5 | before { Lamby::ColdStartMetrics.clear! } 6 | 7 | it 'has a config that defaults to false' do 8 | refute Lamby.config.cold_start_metrics? 9 | end 10 | 11 | it 'calling instrument for the first time will output a CloudWatch count metric for ColdStart' do 12 | out = capture(:stdout) { Lamby::ColdStartMetrics.instrument! } 13 | metric = JSON.parse(out) 14 | expect(metric['AppName']).must_equal 'Dummy' 15 | expect(metric['ColdStart']).must_equal 1 16 | metrics = metric['_aws']['CloudWatchMetrics'] 17 | expect(metrics.size).must_equal 1 18 | expect(metrics.first['Namespace']).must_equal 'Lamby' 19 | expect(metrics.first['Dimensions']).must_equal [['AppName']] 20 | expect(metrics.first['Metrics']).must_equal [{'Name' => 'ColdStart', 'Unit' => 'Count'}] 21 | end 22 | 23 | it 'only ever sends one metric for the lifespan of the function' do 24 | assert_output(/ColdStart/) { Lamby::ColdStartMetrics.instrument! } 25 | assert_output('') { Lamby::ColdStartMetrics.instrument! } 26 | Timecop.travel(Time.now + 10) { assert_output('') { Lamby::ColdStartMetrics.instrument! } } 27 | Timecop.travel(Time.now + 50000000) { assert_output('') { Lamby::ColdStartMetrics.instrument! } } 28 | end 29 | 30 | it 'will record a ProactiveInit metric if the function is called after 10 seconds' do 31 | Timecop.travel(Time.now + 11) do 32 | out = capture(:stdout) { Lamby::ColdStartMetrics.instrument! } 33 | metric = JSON.parse(out) 34 | expect(metric['AppName']).must_equal 'Dummy' 35 | expect(metric['ProactiveInit']).must_equal 1 36 | metrics = metric['_aws']['CloudWatchMetrics'] 37 | expect(metrics.size).must_equal 1 38 | expect(metrics.first['Namespace']).must_equal 'Lamby' 39 | expect(metrics.first['Dimensions']).must_equal [['AppName']] 40 | expect(metrics.first['Metrics']).must_equal [{'Name' => 'ProactiveInit', 'Unit' => 'Count'}] 41 | end 42 | end 43 | 44 | it 'will not record a ProactiveInit metric if the function is called before 10 seconds' do 45 | Timecop.travel(Time.now + 9) do 46 | assert_output(/ColdStart/) { Lamby::ColdStartMetrics.instrument! } 47 | end 48 | end 49 | 50 | private 51 | 52 | def now_ms 53 | (Time.now.to_f * 1000).to_i 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /test/debug_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DebugTest < LambySpec 4 | describe '#on?' do 5 | let(:event) do 6 | TestHelpers::Events::HttpV2.create( 7 | 'queryStringParameters' => { 8 | 'debug' => debug_param 9 | } 10 | ) 11 | end 12 | 13 | before do 14 | @old_rack_env = ENV['RACK_ENV'] 15 | @old_rails_env = ENV['RAILS_ENV'] 16 | ENV['RACK_ENV'] = rack_env 17 | ENV['RAILS_ENV'] = rails_env 18 | end 19 | 20 | after do 21 | ENV['RACK_ENV'] = @old_rack_env 22 | ENV['RAILS_ENV'] = @old_rails_env 23 | end 24 | 25 | describe 'when RACK_ENV == development' do 26 | let(:rack_env) { 'development' } 27 | let(:rails_env) { nil } 28 | 29 | describe 'when the debug param is 1' do 30 | let(:debug_param) { '1' } 31 | 32 | it 'returns true' do 33 | expect(Lamby::Debug.on?(event)).must_equal true 34 | end 35 | end 36 | 37 | describe 'when the debug param is nil' do 38 | let(:debug_param) { nil } 39 | 40 | it 'returns false' do 41 | expect(Lamby::Debug.on?(event)).must_equal false 42 | end 43 | end 44 | end 45 | 46 | describe 'when RAILS_ENV == development' do 47 | let(:rack_env) { 'production' } 48 | let(:rails_env) { 'development' } 49 | 50 | describe 'when the debug param is 1' do 51 | let(:debug_param) { '1' } 52 | 53 | it 'returns true' do 54 | expect(Lamby::Debug.on?(event)).must_equal true 55 | end 56 | end 57 | 58 | describe 'when the debug param is nil' do 59 | let(:debug_param) { nil } 60 | 61 | it 'returns false' do 62 | expect(Lamby::Debug.on?(event)).must_equal false 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/dummy_app/Rakefile: -------------------------------------------------------------------------------- 1 | require_relative 'init' 2 | Dummy::Application.load_tasks 3 | -------------------------------------------------------------------------------- /test/dummy_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link application.css 3 | //= link application.js 4 | -------------------------------------------------------------------------------- /test/dummy_app/app/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/app/controllers/.keep -------------------------------------------------------------------------------- /test/dummy_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | helper_method :logged_in? 3 | 4 | def index 5 | render 6 | end 7 | 8 | def image 9 | data = File.read Rails.root.join('app/images/1.png') 10 | send_data data, type: 'image/png', disposition: 'inline' 11 | end 12 | 13 | def login 14 | if params[:password] == 'password' 15 | session[:logged_in] = 'true' 16 | end 17 | redirect_to root_url 18 | end 19 | 20 | def logout 21 | reset_session 22 | redirect_to root_url 23 | end 24 | 25 | def exception 26 | raise 'hell' 27 | end 28 | 29 | def percent 30 | render 31 | end 32 | 33 | def cooks 34 | cookies['1'] = '1' 35 | cookies['2'] = '2' 36 | render :index 37 | end 38 | 39 | private 40 | 41 | def logged_in? 42 | session[:logged_in] == 'true' 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/dummy_app/app/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/app/images/1.png -------------------------------------------------------------------------------- /test/dummy_app/app/models/dummy/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/app/models/dummy/.keep -------------------------------------------------------------------------------- /test/dummy_app/app/views/application/index.html.erb: -------------------------------------------------------------------------------- 1 |

Hello Lamby

2 | -------------------------------------------------------------------------------- /test/dummy_app/app/views/application/percent.html.erb: -------------------------------------------------------------------------------- 1 | Params: <%= params[:path] %> 2 | Request Path: <%= request.path %> 3 | -------------------------------------------------------------------------------- /test/dummy_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= yield %> 7 |
<%= logged_in? %>
8 | 9 | 10 | -------------------------------------------------------------------------------- /test/dummy_app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/dummy_app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rake' 3 | Rake.application.run 4 | -------------------------------------------------------------------------------- /test/dummy_app/config.ru: -------------------------------------------------------------------------------- 1 | require ::File.expand_path('../init', __FILE__) 2 | run Dummy::Application 3 | -------------------------------------------------------------------------------- /test/dummy_app/config/boot.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/config/boot.rb -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/config/initializers/.keep -------------------------------------------------------------------------------- /test/dummy_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.config.secret_key_base = '012345678901234567890123456789' 2 | -------------------------------------------------------------------------------- /test/dummy_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | root to: 'application#index' 3 | get 'image' => 'application#image' 4 | post 'login', to: 'application#login' 5 | delete 'logout', to: 'application#logout' 6 | get 'exception', to: 'application#exception' 7 | get 'percent/*path', to: 'application#percent' 8 | get 'cooks', to: 'application#cooks' 9 | get 'redirect_test', to: redirect('/') 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy_app/init.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | ENV["RAILS_SERVE_STATIC_FILES"] = '1' 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __FILE__) 4 | require 'bundler/setup' 5 | require 'rails' 6 | require 'active_model/railtie' 7 | require 'active_job/railtie' 8 | require 'action_controller/railtie' 9 | require 'action_mailer/railtie' 10 | require 'action_view/railtie' 11 | require 'rails/test_unit/railtie' 12 | Bundler.require(:default, Rails.env) 13 | 14 | module Dummy 15 | class Application < ::Rails::Application 16 | 17 | # Basic Engine 18 | config.root = File.join __FILE__, '..' 19 | config.cache_store = :memory_store 20 | config.secret_key_base = '012345678901234567890123456789' 21 | config.active_support.test_order = :random 22 | config.logger = Logger.new('/dev/null') 23 | config.public_file_server.enabled = true 24 | config.public_file_server.headers = { 25 | 'Cache-Control' => "public, max-age=2592000", 26 | 'X-Lamby-Base64' => '1' 27 | } 28 | # Mimic production environment. 29 | config.consider_all_requests_local = false 30 | config.action_dispatch.show_exceptions = :all 31 | # Mimic test environment. 32 | config.action_controller.perform_caching = false 33 | config.action_controller.allow_forgery_protection = false 34 | config.action_mailer.delivery_method = :test 35 | config.active_support.deprecation = :stderr 36 | config.allow_concurrency = true 37 | config.cache_classes = true 38 | config.dependency_loading = true 39 | config.preload_frameworks = true 40 | config.eager_load = true 41 | config.middleware.insert_after ActionDispatch::Static, Rack::Deflater, sync: false if ENV['LAMBY_RACK_DEFLATE_ENABLED'] 42 | config.active_job.queue_adapter = :lambdakiq 43 | end 44 | end 45 | 46 | Dummy::Application.initialize! 47 | -------------------------------------------------------------------------------- /test/dummy_app/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/public/.keep -------------------------------------------------------------------------------- /test/dummy_app/public/1-public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/public/1-public.png -------------------------------------------------------------------------------- /test/dummy_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy_app/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy_app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rails-lambda/lamby/95d083e1568597f31841ad8920d71a44f2e694e5/test/dummy_app/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /test/handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HandlerTest < LambySpec 4 | 5 | let(:app) { Rack::Builder.new { run Rails.application }.to_app } 6 | let(:context) { TestHelpers::LambdaContext.new } 7 | 8 | 9 | describe 'http-v2' do 10 | it 'returns the correct rack response' do 11 | event = TestHelpers::Events::HttpV2.create 12 | handler = Lamby::Handler.new(app, event, context, rack: :http) 13 | handler.call 14 | response = handler.send(:rack_response) 15 | 16 | expect(response[:statusCode]).must_equal 200 17 | expect(response[:headers]['Content-Type']).must_equal 'text/html; charset=utf-8' 18 | expect(response[:body]).must_match %r{

Hello Lamby

} 19 | expect(response.keys).must_equal [:statusCode, :headers, :body] 20 | end 21 | 22 | it 'get' do 23 | event = TestHelpers::Events::HttpV2.create 24 | result = Lamby.handler app, event, context, rack: :http 25 | expect(result[:statusCode]).must_equal 200 26 | expect(result[:body]).must_match %r{

Hello Lamby

} 27 | expect(result[:body]).must_match %r{
false
} 28 | end 29 | 30 | it 'head' do 31 | event = TestHelpers::Events::HttpV2.create( 32 | 'requestContext' => {'http' => {'method' => 'HEAD'}}, 33 | 'body' => nil 34 | ) 35 | result = Lamby.handler app, event, context, rack: :http 36 | expect(result[:statusCode]).must_equal 200 37 | expect(result[:body]).must_equal "" 38 | end 39 | 40 | it 'get - multiple cookies' do 41 | event = TestHelpers::Events::HttpV2.create( 42 | 'rawPath' => '/production/cooks', 43 | 'requestContext' => { 'http' => {'path' => '/production/cooks'} } 44 | ) 45 | result = Lamby.handler app, event, context, rack: :http 46 | expect(result[:statusCode]).must_equal 200 47 | expect(result[:headers]['Set-Cookie']).must_be_nil 48 | expect(result[:cookies]).must_equal ["1=1; path=/", "2=2; path=/"] 49 | expect(result[:body]).must_match %r{

Hello Lamby

} 50 | end 51 | 52 | it 'get - image' do 53 | event = TestHelpers::Events::HttpV2.create( 54 | 'rawPath' => '/production/image', 55 | 'requestContext' => { 'http' => {'path' => '/production/image'} } 56 | ) 57 | result = Lamby.handler app, event, context, rack: :http 58 | expect(result[:statusCode]).must_equal 200 59 | expect(result[:body]).must_equal encode64(dummy_app_image) 60 | expect(result[:headers]['content-type']).must_equal 'image/png' 61 | expect(result[:isBase64Encoded]).must_equal true 62 | # Public file server. 63 | event = TestHelpers::Events::HttpV2.create( 64 | 'rawPath' => '/production/1-public.png', 65 | 'requestContext' => { 'http' => {'path' => '/production/1-public.png'} } 66 | ) 67 | result = Lamby.handler app, event, context, rack: :http 68 | expect(result[:statusCode]).must_equal 200 69 | expect(result[:body]).must_equal encode64(dummy_app_image_public), 'not' 70 | expect(result[:headers]['content-type']).must_equal 'image/png' 71 | expect(result[:headers]['Cache-Control']).must_equal 'public, max-age=2592000' 72 | expect(result[:headers]['X-Lamby-Base64']).must_equal '1' 73 | expect(result[:isBase64Encoded]).must_equal true 74 | end 75 | 76 | it 'post - login' do 77 | event = TestHelpers::Events::HttpV2Post.create 78 | result = Lamby.handler app, event, context, rack: :http 79 | expect(result[:statusCode]).must_equal 302 80 | expect(result[:headers]['Location']).must_equal 'https://myawesomelambda.example.com/' 81 | # Check logged in state via GET. 82 | event = TestHelpers::Events::HttpV2.create( 83 | 'cookies' => [session_cookie(result)] 84 | ) 85 | result = Lamby.handler app, event, context, rack: :http 86 | expect(result[:statusCode]).must_equal 200 87 | expect(result[:body]).must_match %r{
true
} 88 | end 89 | 90 | it 'get - exception' do 91 | event = TestHelpers::Events::HttpV2.create( 92 | 'rawPath' => '/production/exception', 93 | 'requestContext' => { 'http' => {'path' => '/production/exception'} } 94 | ) 95 | result = Lamby.handler app, event, context, rack: :http 96 | expect(result[:statusCode]).must_equal 500 97 | expect(result[:body]).must_match %r{We're sorry, but something went wrong.} 98 | expect(result[:body]).must_match %r{This file lives in public/500.html} 99 | end 100 | 101 | it 'get - percent' do 102 | event = TestHelpers::Events::HttpV2.create( 103 | 'rawPath' => '/production/percent/dwef782jkif%3d', 104 | 'requestContext' => { 'http' => {'path' => '/production/percent/dwef782jkif='} } 105 | ) 106 | result = Lamby.handler app, event, context, rack: :http 107 | expect(result[:statusCode]).must_equal 200 108 | expect(result[:body]).must_match %r{Params: dwef782jkif=} 109 | expect(result[:body]).must_match %r{Request Path: /percent/dwef782jkif%3} 110 | end 111 | 112 | end 113 | 114 | describe 'http-v1' do 115 | 116 | it 'returns the correct rack response' do 117 | event = TestHelpers::Events::HttpV1.create 118 | handler = Lamby::Handler.new(app, event, context, rack: :http) 119 | handler.call 120 | response = handler.send(:rack_response) 121 | 122 | expect(response[:statusCode]).must_equal 200 123 | expect(response[:headers]['Content-Type']).must_equal 'text/html; charset=utf-8' 124 | expect(response[:body]).must_match %r{

Hello Lamby

} 125 | expect(response.keys).must_equal [:statusCode, :headers, :body] 126 | end 127 | 128 | it 'get' do 129 | event = TestHelpers::Events::HttpV1.create 130 | result = Lamby.handler app, event, context, rack: :http 131 | expect(result[:statusCode]).must_equal 200 132 | expect(result[:body]).must_match %r{

Hello Lamby

} 133 | expect(result[:body]).must_match %r{
false
} 134 | end 135 | 136 | it 'head' do 137 | event = TestHelpers::Events::HttpV1.create( 138 | 'httpMethod' => 'HEAD', 139 | 'requestContext' => {'httpMethod' => 'HEAD'}, 140 | 'body' => nil 141 | ) 142 | result = Lamby.handler app, event, context, rack: :http 143 | expect(result[:statusCode]).must_equal 200 144 | expect(result[:body]).must_equal "" 145 | end 146 | 147 | it 'get - multiple cookies' do 148 | event = TestHelpers::Events::HttpV1.create( 149 | 'path' => '/production/cooks', 150 | 'requestContext' => { 'path' => '/production/cooks'} 151 | ) 152 | result = Lamby.handler app, event, context, rack: :http 153 | expect(result[:statusCode]).must_equal 200 154 | expect(result[:headers]['Set-Cookie']).must_be_nil 155 | expect(result[:multiValueHeaders]['Set-Cookie']).must_equal ["1=1; path=/", "2=2; path=/"] 156 | expect(result[:body]).must_match %r{

Hello Lamby

} 157 | end 158 | 159 | it 'get - image' do 160 | event = TestHelpers::Events::HttpV1.create( 161 | 'path' => '/production/image', 162 | 'requestContext' => { 'path' => '/production/image' } 163 | ) 164 | result = Lamby.handler app, event, context, rack: :http 165 | expect(result[:statusCode]).must_equal 200 166 | expect(result[:body]).must_equal encode64(dummy_app_image) 167 | expect(result[:headers]['content-type']).must_equal 'image/png' 168 | # Public file server. 169 | event = TestHelpers::Events::HttpV1.create( 170 | 'path' => '/production/1-public.png', 171 | 'requestContext' => { 'path' => '/production/1-public.png' } 172 | ) 173 | result = Lamby.handler app, event, context, rack: :http 174 | expect(result[:statusCode]).must_equal 200 175 | expect(result[:body]).must_equal encode64(dummy_app_image_public), 'not' 176 | expect(result[:headers]['content-type']).must_equal 'image/png' 177 | expect(result[:headers]['Cache-Control']).must_equal 'public, max-age=2592000' 178 | expect(result[:headers]['X-Lamby-Base64']).must_equal '1' 179 | expect(result[:isBase64Encoded]).must_equal true 180 | end 181 | 182 | it 'post - login' do 183 | event = TestHelpers::Events::HttpV1Post.create 184 | result = Lamby.handler app, event, context, rack: :http 185 | expect(result[:statusCode]).must_equal 302 186 | expect(result[:headers]['Location']).must_equal 'https://myawesomelambda.example.com/' 187 | # Check logged in state via GET. 188 | event = TestHelpers::Events::HttpV1.create( 189 | 'headers' => { 'cookie' => session_cookie(result) }, 190 | 'multiValueHeaders' => { 'cookie' => [ session_cookie(result) ]} 191 | ) 192 | result = Lamby.handler app, event, context, rack: :http 193 | expect(result[:statusCode]).must_equal 200 194 | expect(result[:body]).must_match %r{
true
} 195 | end 196 | 197 | it 'get - exception' do 198 | event = TestHelpers::Events::HttpV1.create( 199 | 'path' => '/production/exception', 200 | 'requestContext' => { 'path' => '/production/exception' } 201 | ) 202 | result = Lamby.handler app, event, context, rack: :http 203 | expect(result[:statusCode]).must_equal 500 204 | expect(result[:body]).must_match %r{We're sorry, but something went wrong.} 205 | expect(result[:body]).must_match %r{This file lives in public/500.html} 206 | end 207 | 208 | end 209 | 210 | describe 'rest' do 211 | 212 | it 'get' do 213 | event = TestHelpers::Events::Rest.create 214 | result = Lamby.handler app, event, context, rack: :rest 215 | expect(result[:statusCode]).must_equal 200 216 | expect(result[:body]).must_match %r{

Hello Lamby

} 217 | expect(result[:body]).must_match %r{
false
} 218 | end 219 | 220 | it 'head' do 221 | event = TestHelpers::Events::Rest.create( 222 | 'httpMethod' => 'HEAD', 223 | 'body' => nil 224 | ) 225 | result = Lamby.handler app, event, context, rack: :rest 226 | expect(result[:statusCode]).must_equal 200 227 | expect(result[:body]).must_equal "" 228 | end 229 | 230 | it 'get - multiple cookies' do 231 | event = TestHelpers::Events::Rest.create( 232 | 'path' => '/cooks', 233 | 'requestContext' => { 'path' => '/cooks'} 234 | ) 235 | result = Lamby.handler app, event, context, rack: :rest 236 | expect(result[:statusCode]).must_equal 200 237 | expect(result[:headers]['Set-Cookie']).must_be_nil 238 | expect(result[:multiValueHeaders]['Set-Cookie']).must_equal ["1=1; path=/", "2=2; path=/"] 239 | expect(result[:body]).must_match %r{

Hello Lamby

} 240 | end 241 | 242 | it 'get - image' do 243 | event = TestHelpers::Events::Rest.create( 244 | 'path' => '/image', 245 | 'requestContext' => { 'path' => '/image' } 246 | ) 247 | result = Lamby.handler app, event, context, rack: :rest 248 | expect(result[:statusCode]).must_equal 200 249 | expect(result[:body]).must_equal encode64(dummy_app_image) 250 | expect(result[:headers]['content-type']).must_equal 'image/png' 251 | # Public file server. 252 | event = TestHelpers::Events::Rest.create( 253 | 'path' => '/1-public.png', 254 | 'requestContext' => { 'path' => '/1-public.png' } 255 | ) 256 | result = Lamby.handler app, event, context, rack: :rest 257 | expect(result[:statusCode]).must_equal 200 258 | expect(result[:body]).must_equal encode64(dummy_app_image_public), 'not' 259 | expect(result[:headers]['content-type']).must_equal 'image/png' 260 | expect(result[:headers]['Cache-Control']).must_equal 'public, max-age=2592000' 261 | expect(result[:headers]['X-Lamby-Base64']).must_equal '1' 262 | expect(result[:isBase64Encoded]).must_equal true 263 | end 264 | 265 | it 'post - login' do 266 | event = TestHelpers::Events::RestPost.create 267 | result = Lamby.handler app, event, context, rack: :rest 268 | expect(result[:statusCode]).must_equal 302 269 | expect(result[:headers]['Location']).must_equal 'https://myawesomelambda.example.com/' 270 | # Check logged in state via GET. 271 | event = TestHelpers::Events::Rest.create( 272 | 'headers' => { 'Cookie' => session_cookie(result) }, 273 | 'multiValueHeaders' => { 'Cookie' => [ session_cookie(result) ]} 274 | ) 275 | result = Lamby.handler app, event, context, rack: :rest 276 | expect(result[:statusCode]).must_equal 200 277 | expect(result[:body]).must_match %r{
true
} 278 | end 279 | 280 | it 'get - exception' do 281 | event = TestHelpers::Events::Rest.create( 282 | 'path' => '/exception', 283 | 'requestContext' => { 'path' => '/exception' } 284 | ) 285 | result = Lamby.handler app, event, context, rack: :rest 286 | expect(result[:statusCode]).must_equal 500 287 | expect(result[:body]).must_match %r{We're sorry, but something went wrong.} 288 | expect(result[:body]).must_match %r{This file lives in public/500.html} 289 | end 290 | 291 | end 292 | 293 | describe 'alb' do 294 | 295 | it 'get' do 296 | event = TestHelpers::Events::Alb.create 297 | result = Lamby.handler app, event, context, rack: :alb 298 | expect(result[:statusCode]).must_equal 200 299 | expect(result[:body]).must_match %r{

Hello Lamby

} 300 | expect(result[:body]).must_match %r{
false
} 301 | end 302 | 303 | it 'head' do 304 | event = TestHelpers::Events::Alb.create( 305 | 'httpMethod' => 'HEAD', 306 | 'body' => nil 307 | ) 308 | result = Lamby.handler app, event, context, rack: :alb 309 | expect(result[:statusCode]).must_equal 200 310 | expect(result[:body]).must_equal "" 311 | end 312 | 313 | it 'get - multiple cookies' do 314 | event = TestHelpers::Events::Alb.create 'path' => '/cooks' 315 | result = Lamby.handler app, event, context, rack: :rest 316 | expect(result[:statusCode]).must_equal 200 317 | expect(result[:headers]['Set-Cookie']).must_be_nil 318 | expect(result[:multiValueHeaders]['Set-Cookie']).must_equal ["1=1; path=/", "2=2; path=/"] 319 | expect(result[:body]).must_match %r{

Hello Lamby

} 320 | end 321 | 322 | it 'get - image' do 323 | event = TestHelpers::Events::Alb.create 'path' => '/image' 324 | result = Lamby.handler app, event, context, rack: :alb 325 | expect(result[:statusCode]).must_equal 200 326 | expect(result[:body]).must_equal encode64(dummy_app_image) 327 | expect(result[:headers]['content-type']).must_equal 'image/png' 328 | event = TestHelpers::Events::Alb.create 'path' => '/1-public.png' 329 | result = Lamby.handler app, event, context, rack: :alb 330 | expect(result[:statusCode]).must_equal 200 331 | expect(result[:body]).must_equal encode64(dummy_app_image_public), 'not' 332 | expect(result[:headers]['content-type']).must_equal 'image/png' 333 | expect(result[:headers]['Cache-Control']).must_equal 'public, max-age=2592000' 334 | expect(result[:headers]['X-Lamby-Base64']).must_equal '1' 335 | expect(result[:isBase64Encoded]).must_equal true 336 | end 337 | 338 | it 'get - exception' do 339 | event = TestHelpers::Events::Alb.create( 340 | 'path' => '/exception', 341 | 'requestContext' => { 'path' => '/exception' } 342 | ) 343 | result = Lamby.handler app, event, context, rack: :alb 344 | expect(result[:statusCode]).must_equal 500 345 | expect(result[:body]).must_match %r{We're sorry, but something went wrong.} 346 | expect(result[:body]).must_match %r{This file lives in public/500.html} 347 | end 348 | 349 | end 350 | 351 | describe 'event_bridge' do 352 | 353 | let(:event) do 354 | { 355 | "version" => "0", 356 | "id"=>"0874bcac-1dac-2393-637f-201025f217b0", 357 | "detail-type"=>"orderCreated", 358 | "source"=>"com.myorg.stores", 359 | "account"=>"123456789012", 360 | "time"=>"2021-04-29T13:51:41Z", 361 | "region"=>"us-east-1", 362 | "resources"=>[], 363 | "detail"=>{"id" => "123"} 364 | } 365 | end 366 | 367 | it 'has a configurable proc' do 368 | expect(Lamby.config.event_bridge_handler).must_be_instance_of Proc 369 | Lamby.config.event_bridge_handler = lambda { |e,c| "#{e}#{c}" } 370 | r = Lamby.config.event_bridge_handler.call(1,2) 371 | expect(r).must_equal '12' 372 | end 373 | 374 | it 'basic event puts to log' do 375 | out = capture(:stdout) { @result = Lamby.handler app, event, context } 376 | expect(out).must_match %r{0874bcac-1dac-2393-637f-201025f217b0} 377 | end 378 | 379 | end 380 | 381 | describe 'lambdakiq' do 382 | 383 | let(:event) do 384 | { 385 | "Records" => [ 386 | { 387 | "messageId" => "9081fe74-bc79-451f-a03a-2fe5c6e2f807", 388 | "receiptHandle" => "AQEBgbn8GmF1fMo4z3IIqlJYymS6e7NBynwE+LsQlzjjdcKtSIomGeKMe0noLC9UDShUSe8bzr0s+pby03stHNRv1hgg4WRB5YT4aO0dwOuio7LvMQ/VW88igQtWmca78K6ixnU9X5Sr6J+/+WMvjBgIdvO0ycAM2tyJ1nxRHs/krUoLo/bFCnnwYh++T5BLQtFjFGrRkPjWnzjAbLWKU6Hxxr5lkHSxGhjfAoTCOjhi9crouXaWD+H1uvoGx/O/ZXaeMNjKIQoKjhFguwbEpvrq2Pfh2x9nRgBP3cKa9qw4Q3oFQ0MiQAvnK+UO8cCnsKtD", 389 | "body" => "{\"job_class\":\"TestHelpers::Jobs::BasicJob\",\"job_id\":\"527cd37e-08f4-4aa8-9834-a46220cdc5a3\",\"provider_job_id\":null,\"queue_name\":\"lambdakiq-JobsQueue-TESTING123.fifo\",\"priority\":null,\"arguments\":[\"test\"],\"executions\":0,\"exception_executions\":{},\"locale\":\"en\",\"timezone\":\"UTC\",\"enqueued_at\":\"2020-11-30T13:07:36Z\"}", 390 | "attributes" => { 391 | "ApproximateReceiveCount" => "1", 392 | "SentTimestamp" => "1606741656429", 393 | "SequenceNumber" => "18858069937755376128", 394 | "MessageGroupId" => "527cd37e-08f4-4aa8-9834-a46220cdc5a3", 395 | "SenderId" => "AROA4DJKY67RBVYCN5UZ3", 396 | "MessageDeduplicationId" => "527cd37e-08f4-4aa8-9834-a46220cdc5a3", 397 | "ApproximateFirstReceiveTimestamp" => "1606741656429" 398 | }, 399 | "messageAttributes" => { 400 | "lambdakiq" => { 401 | "stringValue" => "1", 402 | "stringListValues" => [], 403 | "binaryListValues" => [], 404 | "dataType" => "String" 405 | } 406 | }, 407 | "md5OfMessageAttributes" => "5fde2d817e4e6b7f28735d3b1725f817", 408 | "md5OfBody" => "6477b54fb64dde974ea7514e87d3b8a5", 409 | "eventSource" => "aws:sqs", 410 | "eventSourceARN" => "arn:aws:sqs:us-east-1:831702759394:lambdakiq-JobsQueue-TESTING123.fifo", "awsRegion" => "us-east-1" 411 | } 412 | ] 413 | } 414 | end 415 | 416 | it 'basic event' do 417 | out = capture(:stdout) { @result = Lamby.handler app, event, context } 418 | expect(out).must_match %r{BasicJob with: "test"} 419 | expect(@result).must_be_instance_of Hash 420 | expect(@result[:batchItemFailures]).must_be_instance_of Array 421 | expect(@result[:batchItemFailures]).must_be_empty 422 | end 423 | 424 | end 425 | 426 | describe 'LambdaConsole' do 427 | 428 | after do 429 | Lamby.config.runner_patterns.push %r{.*} 430 | end 431 | 432 | it 'run' do 433 | event = { 'X_LAMBDA_CONSOLE' => { 'run' => %q|echo 'hello'| } } 434 | result = Lamby.handler app, event, context 435 | expect(result[:statusCode]).must_equal 0 436 | expect(result[:headers]).must_equal({}) 437 | expect(result[:body]).must_match %r{hello} 438 | end 439 | 440 | it 'run with error' do 441 | event = { 'X_LAMBDA_CONSOLE' => { 'run' => %q|/usr/bin/doesnotexist| } } 442 | result = Lamby.handler app, event, context 443 | expect(result[:statusCode]).must_equal 1 444 | expect(result[:headers]).must_equal({}) 445 | expect(result[:body]).must_match %r{No such file or directory} 446 | end 447 | 448 | it 'run with pattern' do 449 | Lamby.config.runner_patterns.clear 450 | event = { 'X_LAMBDA_CONSOLE' => { 'run' => %q|echo 'hello'| } } 451 | error = assert_raises LambdaConsole::Run::UnknownCommandPattern do 452 | Lamby.handler app, event, context 453 | end 454 | expect(error.message).must_equal %|echo 'hello'| 455 | end 456 | 457 | it 'interact' do 458 | event = { 'X_LAMBDA_CONSOLE' => { 'interact' => 'Object.new' } } 459 | result = Lamby.handler app, event, context 460 | expect(result[:statusCode]).must_equal 200 461 | expect(result[:headers]).must_equal({}) 462 | expect(result[:body]).must_match %r{#} 463 | end 464 | 465 | it 'interact with error' do 466 | event = { 'X_LAMBDA_CONSOLE' => { 'interact' => 'raise("hell")' } } 467 | result = Lamby.handler app, event, context 468 | expect(result[:statusCode]).must_equal 422 469 | expect(result[:headers]).must_equal({}) 470 | expect(result[:body]).must_match %r{#} 471 | end 472 | 473 | end 474 | 475 | private 476 | 477 | def session_cookie(result) 478 | cookies = (result[:cookies] || result[:multiValueHeaders]['Set-Cookie'])[0] 479 | cookies.split('; ').detect { |x| x =~ /session=/ } 480 | end 481 | 482 | end 483 | -------------------------------------------------------------------------------- /test/lamby_core_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LambyCoreSpec < LambySpec 4 | def setup 5 | @event = { 'httpMethod' => 'GET', 'path' => '/' } 6 | @context = TestHelpers::LambdaContext.new 7 | end 8 | 9 | it 'has a version number' do 10 | expect(Lamby::VERSION).wont_be_nil 11 | end 12 | 13 | it 'catches SIGTERM signal' do 14 | assert_raises(SystemExit) do 15 | Process.kill('TERM', Process.pid) 16 | sleep 0.1 # Give time for the signal to be processed 17 | end 18 | end 19 | 20 | it 'catches SIGINT signal' do 21 | assert_raises(SystemExit) do 22 | Process.kill('INT', Process.pid) 23 | sleep 0.1 # Give time for the signal to be processed 24 | end 25 | end 26 | 27 | it 'executes cmd method' do 28 | Lamby.config.stubs(:handled_proc).returns(->(_, _) {}) 29 | result = Lamby.cmd(event: @event, context: @context) 30 | 31 | assert result.is_a?(Hash), "Expected result to be a Hash, but got #{result.class}" 32 | assert_equal 200, result[:statusCode], "Expected statusCode to be 200, but got #{result[:statusCode]}" 33 | assert_includes result[:body], "Hello Lamby", "Expected body to contain 'Hello Lamby'" 34 | end 35 | 36 | it 'executes handler method' do 37 | app = Rack::Builder.new do 38 | run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } 39 | end.to_app 40 | 41 | event = {'httpMethod' => 'GET'} 42 | result = Lamby.handler(app, event, @context) 43 | 44 | assert result.is_a?(Hash), "Expected result to be a Hash, but got #{result.class}" 45 | assert_equal 200, result[:statusCode], "Expected statusCode to be 200, but got #{result[:statusCode]}" 46 | assert_equal 'OK', result[:body], "Expected body to be 'OK', but got #{result[:body]}" 47 | end 48 | 49 | it 'returns the configuration' do 50 | config = Lamby.config 51 | assert config.is_a?(Lamby::Configuration), "Expected config to be an instance of Lamby::Config, but got #{config.class}" 52 | end 53 | end -------------------------------------------------------------------------------- /test/proxy_context_test.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /test/proxy_server_test.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'test_helper' 3 | 4 | class ProxyServerTest < LambySpec 5 | include Rack::Test::Methods 6 | 7 | let(:event) { TestHelpers::Events::HttpV2.create } 8 | let(:context) { TestHelpers::LambdaContext.raw_data } 9 | let(:app) { Rack::Builder.new { run Lamby::ProxyServer.new }.to_app } 10 | let(:json) { {"event": event, "context": context}.to_json } 11 | 12 | it 'should return a 405 helpful message on GET' do 13 | response = get '/' 14 | expect(response.status).must_equal 405 15 | expect(response.headers).must_equal({"content-type"=>"text/html", "content-length"=>"233"}) 16 | expect(response.body).must_include 'Method Not Allowed' 17 | end 18 | 19 | it 'should call Lamby.cmd on POST and include full response as JSON' do 20 | response = post '/', json, 'CONTENT_TYPE' => 'application/json' 21 | expect(response.status).must_equal 200 22 | expect(response.headers).must_equal({"content-type"=>"application/json", "content-length"=>"740"}) 23 | response_body = JSON.parse(response.body) 24 | expect(response_body['statusCode']).must_equal 200 25 | expect(response_body['headers']).must_be_kind_of Hash 26 | expect(response_body['body']).must_match 'Hello Lamby' 27 | end 28 | 29 | it 'will return whatever Lamby.cmd does' do 30 | Lamby.stubs(:cmd).returns({statusCode: 200}) 31 | response = post '/', json, 'CONTENT_TYPE' => 'application/json' 32 | expect(response.status).must_equal 200 33 | expect(response.headers).must_equal({"content-type"=>"application/json", "content-length"=>"18"}) 34 | response_body = JSON.parse(response.body) 35 | expect(response_body['statusCode']).must_equal 200 36 | expect(response_body['headers']).must_be_nil 37 | expect(response_body['body']).must_be_nil 38 | end 39 | 40 | it 'will use the configured Lamby rack_app' do 41 | rack_app = Rack::Builder.new { run lambda { |env| [200, {}, StringIO.new('OK')] } }.to_app 42 | Lamby.config.rack_app = rack_app 43 | response = post '/', json, 'CONTENT_TYPE' => 'application/json' 44 | expect(response.status).must_equal 200 45 | expect(response.headers).must_equal({"content-type"=>"application/json", "content-length"=>"43"}) 46 | response_body = JSON.parse(response.body) 47 | expect(response_body['statusCode']).must_equal 200 48 | expect(response_body['headers']).must_equal({}) 49 | expect(response_body['body']).must_equal('OK') 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /test/rack_alb_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RackAlbTest < LambySpec 4 | 5 | let(:context) { TestHelpers::LambdaContext.new } 6 | 7 | it 'env' do 8 | event = TestHelpers::Events::Alb.create 9 | rack = Lamby::RackAlb.new event, context 10 | expect(rack.env['REQUEST_METHOD']).must_equal 'GET' 11 | expect(rack.env['SCRIPT_NAME']).must_equal '' 12 | expect(rack.env['PATH_INFO']).must_equal '/' 13 | expect(rack.env['QUERY_STRING']).must_equal 'colors[]=blue&colors[]=red' 14 | expect(rack.env['SERVER_NAME']).must_equal 'myawesomelambda.example.com' 15 | expect(rack.env['SERVER_PORT']).must_equal '443' 16 | expect(rack.env['SERVER_PROTOCOL']).must_equal 'HTTP/1.1' 17 | expect(rack.env['rack.url_scheme']).must_equal 'https' 18 | expect(rack.env['HTTP_ACCEPT']).must_equal 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 19 | expect(rack.env['HTTP_ACCEPT_ENCODING']).must_equal 'gzip' 20 | expect(rack.env['HTTP_HOST']).must_equal 'myawesomelambda.example.com' 21 | expect(rack.env['HTTP_USER_AGENT']).must_equal 'Amazon CloudFront' 22 | expect(rack.env['HTTP_VIA']).must_equal '2.0 3dc5b7040885724e78019cc31f0ef3d9.cloudfront.net (CloudFront)' 23 | expect(rack.env['HTTP_X_AMZ_CF_ID']).must_equal 'BSlDkHoVD8-009TATJzymLqSBzViE_6jj7DlkiJkub-PpDb8wI4Pxw==' 24 | expect(rack.env['HTTP_X_AMZN_TRACE_ID']).must_equal 'Root=1-5e7c160a-0a9065c7a28a428cd8b98215' 25 | expect(rack.env['HTTP_X_FORWARDED_FOR']).must_equal '72.218.219.201, 72.218.219.201, 34.195.252.132' 26 | expect(rack.env['HTTP_X_FORWARDED_PORT']).must_equal '443' 27 | expect(rack.env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' 28 | expect(rack.env['HTTP_X_REQUEST_ID']).must_equal 'a59284fd-d48c-4de5-af9e-df4254489ac2' 29 | expect(rack.env['HTTP_COOKIE']).must_equal 'signal1=test; signal2=control' 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/rack_deflate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RackDeflateTest < LambySpec 4 | 5 | let(:app) { Rack::Builder.new { run Rails.application }.to_app } 6 | let(:context) { TestHelpers::LambdaContext.new } 7 | 8 | it 'get - Rest' do 9 | event = TestHelpers::Events::Rest.create 10 | result = Lamby.handler app, event, context, rack: :rest 11 | expect(result[:statusCode]).must_equal 200 12 | end 13 | 14 | it 'head - Rest' do 15 | event = TestHelpers::Events::Rest.create( 16 | 'httpMethod' => 'HEAD', 17 | 'body' => nil 18 | ) 19 | result = Lamby.handler app, event, context, rack: :rest 20 | expect(result[:statusCode]).must_equal 200 21 | end 22 | 23 | it 'get - Rest with redirect' do 24 | event = TestHelpers::Events::Rest.create( 25 | 'path' => '/redirect_test', 26 | 'requestContext' => { 'path' => '/redirect_test'} 27 | ) 28 | result = Lamby.handler app, event, context, rack: :rest 29 | expect(result[:statusCode]).must_equal 301 30 | refute result[:headers]['Location'].nil? 31 | end 32 | 33 | it 'get - HttpV1' do 34 | event = TestHelpers::Events::HttpV1.create 35 | result = Lamby.handler app, event, context, rack: :http 36 | expect(result[:statusCode]).must_equal 200 37 | end 38 | 39 | it 'head - HttpV1' do 40 | event = TestHelpers::Events::HttpV1.create( 41 | 'httpMethod' => 'HEAD', 42 | 'requestContext' => {'httpMethod' => 'HEAD'}, 43 | 'body' => nil 44 | ) 45 | result = Lamby.handler app, event, context, rack: :http 46 | expect(result[:statusCode]).must_equal 200 47 | end 48 | 49 | it 'get - HttpV1 with redirect' do 50 | event = TestHelpers::Events::HttpV1.create( 51 | 'path' => '/production/redirect_test', 52 | 'requestContext' => { 'path' => '/production/redirect_test'} 53 | ) 54 | result = Lamby.handler app, event, context, rack: :http 55 | expect(result[:statusCode]).must_equal 301 56 | refute result[:headers]['Location'].nil? 57 | end 58 | 59 | it 'get - HttpV2' do 60 | event = TestHelpers::Events::HttpV2.create 61 | result = Lamby.handler app, event, context, rack: :http 62 | expect(result[:statusCode]).must_equal 200 63 | end 64 | 65 | it 'head - HttpV2' do 66 | event = TestHelpers::Events::HttpV2.create( 67 | 'requestContext' => {'http' => {'method' => 'HEAD'}}, 68 | 'body' => nil 69 | ) 70 | result = Lamby.handler app, event, context, rack: :http 71 | expect(result[:statusCode]).must_equal 200 72 | end 73 | 74 | it 'get - HttpV2 with redirect' do 75 | event = TestHelpers::Events::HttpV2.create( 76 | 'rawPath' => '/production/redirect_test', 77 | 'requestContext' => { 'http' => {'path' => '/production/redirect_test'} } 78 | ) 79 | result = Lamby.handler app, event, context, rack: :http 80 | expect(result[:statusCode]).must_equal 301 81 | refute result[:headers]['Location'].nil? 82 | end 83 | 84 | it 'get - Alb' do 85 | event = TestHelpers::Events::Alb.create 86 | result = Lamby.handler app, event, context, rack: :alb 87 | expect(result[:statusCode]).must_equal 200 88 | end 89 | 90 | it 'get - Alb with redirect' do 91 | event = TestHelpers::Events::Alb.create 'path' => '/redirect_test' 92 | result = Lamby.handler app, event, context, rack: :alb 93 | expect(result[:statusCode]).must_equal 301 94 | refute result[:headers]['Location'].nil? 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /test/rack_http_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RackHttpTest < LambySpec 4 | 5 | let(:context) { TestHelpers::LambdaContext.new } 6 | 7 | describe 'v1' do 8 | 9 | it 'env' do 10 | event = TestHelpers::Events::HttpV1.create 11 | rack = Lamby::RackHttp.new event, context 12 | expect(rack.env['REQUEST_METHOD']).must_equal 'GET' 13 | expect(rack.env['SCRIPT_NAME']).must_equal '' 14 | expect(rack.env['PATH_INFO']).must_equal '/' 15 | expect(rack.env['QUERY_STRING']).must_equal 'colors[]=blue&colors[]=red' 16 | expect(rack.env['SERVER_NAME']).must_equal 'myawesomelambda.example.com' 17 | expect(rack.env['SERVER_PORT']).must_equal '443' 18 | expect(rack.env['SERVER_PROTOCOL']).must_equal 'HTTP/1.1' 19 | expect(rack.env['rack.url_scheme']).must_equal 'https' 20 | expect(rack.env['HTTP_ACCEPT']).must_equal 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 21 | expect(rack.env['HTTP_ACCEPT_ENCODING']).must_equal 'gzip, deflate, br' 22 | expect(rack.env['HTTP_HOST']).must_equal 'myawesomelambda.example.com' 23 | expect(rack.env['HTTP_USER_AGENT']).must_equal 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15' 24 | expect(rack.env['HTTP_VIA']).must_be_nil 25 | expect(rack.env['HTTP_X_AMZ_CF_ID']).must_be_nil 26 | expect(rack.env['HTTP_X_AMZN_TRACE_ID']).must_equal 'Root=1-5e7fe714-fee6909429159440eb352c40' 27 | expect(rack.env['HTTP_X_FORWARDED_FOR']).must_equal '72.218.219.201' 28 | expect(rack.env['HTTP_X_FORWARDED_PORT']).must_equal '443' 29 | expect(rack.env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' 30 | expect(rack.env['HTTP_X_REQUEST_ID']).must_equal 'a59284fd-d48c-4de5-af9e-df4254489ac2' 31 | expect(rack.env['HTTP_COOKIE']).must_equal 'signal1=test; signal2=control' 32 | end 33 | 34 | end 35 | 36 | describe 'v2' do 37 | 38 | it 'env' do 39 | event = TestHelpers::Events::HttpV2.create 40 | rack = Lamby::RackHttp.new event, context 41 | expect(rack.env['REQUEST_METHOD']).must_equal 'GET' 42 | expect(rack.env['SCRIPT_NAME']).must_equal '' 43 | expect(rack.env['PATH_INFO']).must_equal '/' 44 | expect(rack.env['QUERY_STRING']).must_equal 'colors[]=blue&colors[]=red' 45 | expect(rack.env['SERVER_NAME']).must_equal 'myawesomelambda.example.com' 46 | expect(rack.env['SERVER_PORT']).must_equal '443' 47 | expect(rack.env['SERVER_PROTOCOL']).must_equal 'HTTP/1.1' 48 | expect(rack.env['rack.url_scheme']).must_equal 'https' 49 | expect(rack.env['HTTP_ACCEPT']).must_equal 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 50 | expect(rack.env['HTTP_ACCEPT_ENCODING']).must_equal 'gzip, deflate, br' 51 | expect(rack.env['HTTP_HOST']).must_equal 'myawesomelambda.example.com' 52 | expect(rack.env['HTTP_USER_AGENT']).must_equal 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15' 53 | expect(rack.env['HTTP_VIA']).must_be_nil 54 | expect(rack.env['HTTP_X_AMZ_CF_ID']).must_be_nil 55 | expect(rack.env['HTTP_X_AMZN_TRACE_ID']).must_equal 'Root=1-5e7fe714-fee6909429159440eb352c40' 56 | expect(rack.env['HTTP_X_FORWARDED_FOR']).must_equal '72.218.219.201' 57 | expect(rack.env['HTTP_X_FORWARDED_PORT']).must_equal '443' 58 | expect(rack.env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' 59 | expect(rack.env['HTTP_X_REQUEST_ID']).must_equal 'a59284fd-d48c-4de5-af9e-df4254489ac2' 60 | expect(rack.env['HTTP_COOKIE']).must_equal 'signal1=test; signal2=control' 61 | end 62 | 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/rack_rest_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RackRestTest < LambySpec 4 | 5 | let(:context) { TestHelpers::LambdaContext.new } 6 | 7 | it 'env' do 8 | event = TestHelpers::Events::Rest.create 9 | rack = Lamby::RackRest.new event, context 10 | expect(rack.env['REQUEST_METHOD']).must_equal 'GET' 11 | expect(rack.env['SCRIPT_NAME']).must_equal '' 12 | expect(rack.env['PATH_INFO']).must_equal '/' 13 | expect(rack.env['QUERY_STRING']).must_equal 'colors[]=blue&colors[]=red' 14 | expect(rack.env['SERVER_NAME']).must_equal '4o8v9z4feh.execute-api.us-east-1.amazonaws.com' 15 | expect(rack.env['SERVER_PORT']).must_equal '443' 16 | expect(rack.env['SERVER_PROTOCOL']).must_equal 'HTTP/1.1' 17 | expect(rack.env['rack.url_scheme']).must_equal 'https' 18 | expect(rack.env['HTTP_ACCEPT']).must_equal 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' 19 | expect(rack.env['HTTP_ACCEPT_ENCODING']).must_equal 'gzip' 20 | expect(rack.env['HTTP_HOST']).must_equal '4o8v9z4feh.execute-api.us-east-1.amazonaws.com' 21 | expect(rack.env['HTTP_USER_AGENT']).must_equal 'Amazon CloudFront' 22 | expect(rack.env['HTTP_VIA']).must_equal '2.0 7f7e359e1c06a914d3d305785359b84d.cloudfront.net (CloudFront)' 23 | expect(rack.env['HTTP_X_AMZ_CF_ID']).must_equal 'kXZzJ72NOsZSsPu-JzNUGyFei1G0r9uzoup3yHrwk4J5qGLKrdUrRA==' 24 | expect(rack.env['HTTP_X_AMZN_TRACE_ID']).must_equal 'Root=1-5e7fe714-fee6909429159440eb352c40' 25 | expect(rack.env['HTTP_X_FORWARDED_FOR']).must_equal '72.218.219.201, 34.195.252.119' 26 | expect(rack.env['HTTP_X_FORWARDED_PORT']).must_equal '443' 27 | expect(rack.env['HTTP_X_FORWARDED_PROTO']).must_equal 'https' 28 | expect(rack.env['HTTP_X_REQUEST_ID']).must_equal 'a59284fd-d48c-4de5-af9e-df4254489ac2' 29 | expect(rack.env['HTTP_COOKIE']).must_equal 'signal1=test; signal2=control' 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/ssm_parameter_store_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SsmParameterStoreTest < LambySpec 4 | 5 | let(:klass) { Lamby::SsmParameterStore } 6 | let(:path) { path_debug || '/config/staging/app/env' } 7 | let(:file) { File.expand_path('../.env.staging', __FILE__) } 8 | 9 | after { clear! } 10 | 11 | describe '#to_env' do 12 | before do 13 | ENV['FOO'] = 'test' 14 | end 15 | 16 | it 'overwrites existing environment variables by default' do 17 | envs = klass.new path 18 | stub_params(envs) 19 | assert ENV.key?('FOO') 20 | refute ENV.key?('BAR') 21 | envs.to_env 22 | expect(ENV['FOO']).must_equal 'foo' 23 | expect(ENV['BAR']).must_equal 'bar' 24 | end 25 | 26 | it 'does not overwrite existing environment variables when overwrite flag set to false' do 27 | envs = klass.new path 28 | stub_params(envs) 29 | assert ENV.key?('FOO') 30 | refute ENV.key?('BAR') 31 | envs.to_env(overwrite: false) 32 | expect(ENV['FOO']).must_equal 'test' 33 | expect(ENV['BAR']).must_equal 'bar' 34 | end 35 | end 36 | 37 | it '#to_dotenv' do 38 | envs = klass.new path, dotenv_file: file 39 | stub_params(envs) 40 | envs.to_dotenv 41 | expect(File.read(file)).must_equal <<~EOF 42 | FOO=foo 43 | BAR=bar 44 | EOF 45 | end 46 | 47 | private 48 | 49 | def stub_params(envs) 50 | envs.stubs(:params).returns([ 51 | param("#{path}/FOO", 'FOO', 'foo'), 52 | param("#{path}/BAR", 'BAR', 'bar') 53 | ]) 54 | end 55 | 56 | def param(name, env, value) 57 | klass::Param.new(name, env, value) 58 | end 59 | 60 | def path_debug 61 | ENV['LAMBY_DEBUG_SSM_PATH'] if debug? 62 | end 63 | 64 | def debug? 65 | ENV['LAMBY_DEBUG_SSM'].present? 66 | end 67 | 68 | def clear! 69 | ENV.delete 'FOO' 70 | ENV.delete 'BAR' 71 | FileUtils.rm_rf(file) 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['LAMBY_TEST'] = '1' 2 | ENV['AWS_EXECUTION_ENV'] = 'AWS_Lambda_Image' 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 4 | require 'lamby' 5 | require 'pry' 6 | require 'timecop' 7 | require 'minitest/autorun' 8 | require 'minitest/focus' 9 | require 'mocha/minitest' 10 | require 'dummy_app/init' 11 | require 'test_helper/dummy_app_helpers' 12 | require 'test_helper/stream_helpers' 13 | require 'test_helper/lambda_context' 14 | require 'test_helper/event_helpers' 15 | require 'test_helper/jobs_helpers' 16 | require 'test_helper/lambdakiq_helpers' 17 | 18 | Rails.backtrace_cleaner.remove_silencers! 19 | Lambdakiq::Client.default_options.merge! stub_responses: true 20 | Timecop.safe_mode = true 21 | 22 | class LambySpec < Minitest::Spec 23 | include TestHelpers::DummyAppHelpers, 24 | TestHelpers::StreamHelpers, 25 | TestHelpers::LambdakiqHelpers 26 | 27 | before do 28 | Lamby.config.reconfigure 29 | lambdakiq_client_reset! 30 | lambdakiq_client_stub_responses 31 | end 32 | 33 | after do 34 | Timecop.return 35 | end 36 | 37 | private 38 | 39 | def encode64(v) 40 | Base64.strict_encode64(v) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/test_helper/dummy_app_helpers.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module DummyAppHelpers 3 | 4 | extend ActiveSupport::Concern 5 | 6 | private 7 | 8 | def dummy_app 9 | ::Dummy::Application 10 | end 11 | 12 | def dummy_root 13 | dummy_app.root 14 | end 15 | 16 | def dummy_config 17 | dummy_app.config 18 | end 19 | 20 | def dummy_app_image 21 | File.read dummy_root.join('app/images/1.png') 22 | end 23 | 24 | def dummy_app_image_public 25 | File.read dummy_root.join('public/1-public.png') 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/test_helper/event_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper/events/base' 2 | require 'test_helper/events/rest' 3 | require 'test_helper/events/rest_post' 4 | require 'test_helper/events/alb' 5 | require 'test_helper/events/http_v1' 6 | require 'test_helper/events/http_v1_post' 7 | require 'test_helper/events/http_v2' 8 | require 'test_helper/events/http_v2_post' 9 | -------------------------------------------------------------------------------- /test/test_helper/events/alb.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class Alb < Base 4 | 5 | self.event = { 6 | "requestContext" => { 7 | "elb" => { 8 | "targetGroupArn" => "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/myawesomelambda-1rndy3u8psl2j/3f1bcbeec09c9050"} 9 | }, 10 | "httpMethod" => "GET", 11 | "path" => "/", 12 | "multiValueQueryStringParameters" => {"colors[]" => ["blue", "red"]}, 13 | "multiValueHeaders" => { 14 | "accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 15 | "accept-encoding" => ["gzip"], 16 | "connection" => ["Keep-Alive"], 17 | "cookie" => ["signal1=test; signal2=control"], 18 | "host" => ["myawesomelambda.example.com"], 19 | "user-agent" => ["Amazon CloudFront"], 20 | "via" => ["2.0 3dc5b7040885724e78019cc31f0ef3d9.cloudfront.net (CloudFront)"], 21 | "x-amz-cf-id" => ["BSlDkHoVD8-009TATJzymLqSBzViE_6jj7DlkiJkub-PpDb8wI4Pxw=="], 22 | "x-amzn-trace-id" => ["Root=1-5e7c160a-0a9065c7a28a428cd8b98215"], 23 | "x-forwarded-for" => ["72.218.219.201", "72.218.219.201", "34.195.252.132"], 24 | "x-forwarded-host" => ["myawesomelambda.example.com"], 25 | "x-forwarded-port" => ["443"], 26 | "x-forwarded-proto" => ["https"] 27 | }, 28 | "body" => "", 29 | "isBase64Encoded" => false 30 | }.freeze 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_helper/events/base.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class Base 4 | 5 | class_attribute :event, instance_writer: false 6 | self.event = Hash.new 7 | 8 | def self.create(overrides = {}) 9 | event.deep_merge(overrides.stringify_keys) 10 | end 11 | 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/test_helper/events/http_v1.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class HttpV1 < Base 4 | # Via Custom Domain Name integration. 5 | # 6 | self.event = { 7 | "version" => "1.0", 8 | "resource" => "$default", 9 | "path" => "/", 10 | "httpMethod" => "GET", 11 | "headers" => { 12 | "Content-Length" => "0", 13 | "Host" => "myawesomelambda.example.com", 14 | "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 15 | "X-Amzn-Trace-Id" => "Root=1-5e7fe714-fee6909429159440eb352c40", 16 | "X-Forwarded-For" => "72.218.219.201", 17 | "X-Forwarded-Port" => "443", 18 | "X-Forwarded-Proto" => "https", 19 | "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 20 | "accept-encoding" => "gzip, deflate, br", 21 | "accept-language" => "en-us", 22 | "cookie" => "signal1=test; signal2=control" 23 | }, 24 | "multiValueHeaders" => { 25 | "Content-Length" => ["0"], 26 | "Host" => ["myawesomelambda.example.com"], 27 | "User-Agent" => ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15"], 28 | "X-Amzn-Trace-Id" => ["Root=1-5e7fe714-fee6909429159440eb352c40"], 29 | "X-Forwarded-For" => ["72.218.219.201"], 30 | "X-Forwarded-Port" => ["443"], 31 | "X-Forwarded-Proto" => ["https"], 32 | "accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 33 | "accept-encoding" => ["gzip, deflate, br"], 34 | "accept-language" => ["en-us"] 35 | }, 36 | "queryStringParameters" => { 37 | "colors[]" => "red" 38 | }, 39 | "multiValueQueryStringParameters" => { 40 | "colors[]" => ["blue", "red"] 41 | }, 42 | "requestContext" => { 43 | "accountId" => nil, 44 | "apiId" => "n12pmpajak", 45 | "domainName" => "myawesomelambda.example.com", 46 | "domainPrefix" => "myawesomelambda", 47 | "extendedRequestId" => "KSCL-irBIAMEJIA=", 48 | "httpMethod" => "GET", 49 | "identity" => { 50 | "accessKey" => nil, 51 | "accountId" => nil, 52 | "caller" => nil, 53 | "cognitoAuthenticationProvider" => nil, 54 | "cognitoAuthenticationType" => nil, 55 | "cognitoIdentityId" => nil, 56 | "cognitoIdentityPoolId" => nil, 57 | "principalOrgId" => nil, 58 | "sourceIp" => "72.218.219.201", 59 | "user" => nil, 60 | "userAgent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 61 | "userArn" => nil 62 | }, 63 | "path" => "/production/", 64 | "protocol" => "HTTP/1.1", 65 | "requestId" => "KSCL-irBIAMEJIA=", 66 | "requestTime" => "01/Apr/2020:00:44:22 +0000", 67 | "requestTimeEpoch" => 1585701862143, 68 | "resourceId" => "$default", 69 | "resourcePath" => "$default", 70 | "stage" => "production" 71 | }, 72 | "pathParameters" => nil, 73 | "stageVariables" => nil, 74 | "body" => nil, 75 | "isBase64Encoded" => false 76 | }.freeze 77 | 78 | # Via CloudFront directly to API Gateway w/Origin Path. 79 | # 80 | # self.event = { 81 | # "version" => "1.0", 82 | # "resource" => "$default", 83 | # "path" => "/production/", 84 | # "httpMethod" => "GET", 85 | # "headers" => { 86 | # "Content-Length" => "0", 87 | # "Host" => "n12pmpajak.execute-api.us-east-1.amazonaws.com", 88 | # "User-Agent" => "Amazon CloudFront", 89 | # "X-Amz-Cf-Id" => "XPTzwjMoVu5Vlp6QLBl_f1NNa2IXRpF_LAFJ9isoq_Pb4MUarItT0w==", 90 | # "X-Amzn-Trace-Id" => "Root=1-5e83cbd2-bfd143859e9214f4860e2779", 91 | # "X-Forwarded-For" => "72.218.219.201, 3.231.2.50", 92 | # "X-Forwarded-Host" => "myawesomelambda.example.com", 93 | # "X-Forwarded-Port" => "443", 94 | # "X-Forwarded-Proto" => "https", 95 | # "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 96 | # "accept-encoding" => "gzip", 97 | # "via" => "2.0 613faec4b883bfe2ebdd8a74d5006f4c.cloudfront.net (CloudFront)" 98 | # }, 99 | # "multiValueHeaders" => { 100 | # "Content-Length" => ["0"], 101 | # "Host" => ["n12pmpajak.execute-api.us-east-1.amazonaws.com"], 102 | # "User-Agent" => ["Amazon CloudFront"], 103 | # "X-Amz-Cf-Id" => ["XPTzwjMoVu5Vlp6QLBl_f1NNa2IXRpF_LAFJ9isoq_Pb4MUarItT0w=="], 104 | # "X-Amzn-Trace-Id" => ["Root=1-5e83cbd2-bfd143859e9214f4860e2779"], 105 | # "X-Forwarded-For" => ["72.218.219.201, 3.231.2.50"], 106 | # "X-Forwarded-Host" => ["myawesomelambda.example.com"], 107 | # "X-Forwarded-Port" => ["443"], 108 | # "X-Forwarded-Proto" => ["https"], 109 | # "accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 110 | # "accept-encoding" => ["gzip"], 111 | # "via" => ["2.0 613faec4b883bfe2ebdd8a74d5006f4c.cloudfront.net (CloudFront)"] 112 | # }, 113 | # "queryStringParameters" => { 114 | # "colors[]" => "red" 115 | # }, 116 | # "multiValueQueryStringParameters" => { 117 | # "colors[]" => ["blue", "red"] 118 | # }, 119 | # "requestContext" => { 120 | # "accountId" => nil, 121 | # "apiId" => "n12pmpajak", 122 | # "domainName" => "n12pmpajak.execute-api.us-east-1.amazonaws.com", 123 | # "domainPrefix" => "n12pmpajak", 124 | # "extendedRequestId" => "KRzI1i2uoAMEPCA=", 125 | # "httpMethod" => "GET", 126 | # "identity" => { 127 | # "accessKey" => nil, 128 | # "accountId" => nil, 129 | # "caller" => nil, 130 | # "cognitoAuthenticationProvider" => nil, 131 | # "cognitoAuthenticationType" => nil, 132 | # "cognitoIdentityId" => nil, 133 | # "cognitoIdentityPoolId" => nil, 134 | # "principalOrgId" => nil, 135 | # "sourceIp" => "3.231.2.50", 136 | # "user" => nil, 137 | # "userAgent" => "Amazon CloudFront", 138 | # "userArn" => nil 139 | # }, 140 | # "path" => "/production/", 141 | # "protocol" => "HTTP/1.1", 142 | # "requestId" => "KRzI1i2uoAMEPCA=", 143 | # "requestTime" => "31/Mar/2020:23:01:38 +0000", 144 | # "requestTimeEpoch" => 1585695698070, 145 | # "resourceId" => "$default", 146 | # "resourcePath" => "$default", 147 | # "stage" => "production" 148 | # }, 149 | # "pathParameters" => nil, 150 | # "stageVariables" => nil, 151 | # "body" => nil, 152 | # "isBase64Encoded" => false 153 | # }.freeze 154 | end 155 | end 156 | end -------------------------------------------------------------------------------- /test/test_helper/events/http_v1_post.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class HttpV1Post < Base 4 | # Via Custom Domain Name integration. 5 | # 6 | self.event = { 7 | "version" => "1.0", 8 | "resource" => "$default", 9 | "path" => "/login", 10 | "httpMethod" => "POST", 11 | "headers" => { 12 | "content-length" => "144", 13 | "content-type" => "application/x-www-form-urlencoded", 14 | "host" => "myawesomelambda.example.com", 15 | "user-agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 16 | "x-amzn-trace-id" => "Root=1-5e83e682-ab153f7a267a9a904369faa6", 17 | "x-forwarded-for" => "72.218.219.201", 18 | "x-forwarded-port" => "443", 19 | "x-forwarded-proto" => "https", 20 | "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 21 | "accept-encoding" => "gzip, deflate, br", 22 | "accept-language" => "en-us", 23 | "cookie" => "signal1=test; signal2=control", 24 | "origin" => "https://myawesomelambda.example.com", 25 | "referer" => "https://myawesomelambda.example.com/?colors[]=blue&colors[]=red" 26 | }, 27 | "multiValueHeaders" => { 28 | "content-length" => ["144"], 29 | "content-type" => ["application/x-www-form-urlencoded"], 30 | "host" => ["myawesomelambda.example.com"], 31 | "user-agent" => ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15"], 32 | "x-amzn-trace-id" => ["Root=1-5e83e682-ab153f7a267a9a904369faa6"], 33 | "x-forwarded-for" => ["72.218.219.201"], 34 | "x-forwarded-port" => ["443"], 35 | "x-forwarded-proto" => ["https"], 36 | "accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 37 | "accept-encoding" => ["gzip, deflate, br"], 38 | "accept-language" => ["en-us"], 39 | "cookie" => ["signal1=test; signal2=control"], 40 | "origin" => ["https://myawesomelambda.example.com"], 41 | "referer" => ["https://myawesomelambda.example.com/?colors[]=blue&colors[]=red"] 42 | }, 43 | "queryStringParameters" => nil, 44 | "multiValueQueryStringParameters" => nil, 45 | "requestContext" => { 46 | "accountId" => nil, 47 | "apiId" => "n12pmpajak", 48 | "domainName" => "myawesomelambda.example.com", 49 | "domainPrefix" => "myawesomelambda", 50 | "extendedRequestId" => "KSD0ZgV2oAMESNg=", 51 | "httpMethod" => "POST", 52 | "identity" => { 53 | "accessKey" => nil, 54 | "accountId" => nil, 55 | "caller" => nil, 56 | "cognitoAuthenticationProvider" => nil, 57 | "cognitoAuthenticationType" => nil, 58 | "cognitoIdentityId" => nil, 59 | "cognitoIdentityPoolId" => nil, 60 | "principalOrgId" => nil, 61 | "sourceIp" => "72.218.219.201", 62 | "user" => nil, 63 | "userAgent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 64 | "userArn" => nil 65 | }, 66 | "path" => "/production/login", 67 | "protocol" => "HTTP/1.1", 68 | "requestId" => "KSD0ZgV2oAMESNg=", 69 | "requestTime" => "01/Apr/2020:00:55:30 +0000", 70 | "requestTimeEpoch" => 1585702530405, 71 | "resourceId" => "$default", 72 | "resourcePath" => "$default", 73 | "stage" => "production" 74 | }, 75 | "pathParameters" => nil, 76 | "stageVariables" => nil, 77 | "body" => "YXV0aGVudGljaXR5X3Rva2VuPVBNbThhY1FCZkZsVTBtbVltbTdOZnhmNktxJTJGWDRlVHRuMXIwYngzWWlJemFjeTRlUURVbk13MHJFUG5GNXVZbk9MRm5QbnlCcXQxVFlzemRMSDBtOUElM0QlM0QmcGFzc3dvcmQ9cGFzc3dvcmQmY29tbWl0PUxvZ2lu", 78 | "isBase64Encoded" => true 79 | }.freeze 80 | end 81 | end 82 | end -------------------------------------------------------------------------------- /test/test_helper/events/http_v2.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class HttpV2 < Base 4 | 5 | # Via Custom Domain Name integration. 6 | # 7 | self.event = { 8 | "version" => "2.0", 9 | "routeKey" => "$default", 10 | "rawPath" => "/production/", 11 | "rawQueryString" => "colors[]=blue&colors[]=red", 12 | "cookies" => ["signal1=test", "signal2=control"], 13 | "headers" => { 14 | "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 15 | "accept-encoding" => "gzip, deflate, br", 16 | "accept-language" => "en-us", 17 | "content-length" => "0", 18 | "host" => "myawesomelambda.example.com", 19 | "user-agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 20 | "x-amzn-trace-id" => "Root=1-5e7fe714-fee6909429159440eb352c40", 21 | "x-forwarded-for" => "72.218.219.201", 22 | "x-forwarded-port" => "443", 23 | "x-forwarded-proto" => "https" 24 | }, 25 | "requestContext" => { 26 | "accountId" => nil, 27 | "apiId" => "n12pmpajak", 28 | "domainName" => "myawesomelambda.example.com", 29 | "domainPrefix" => "myawesomelambda", 30 | "http" => { 31 | "method" => "GET", 32 | "path" => "/production/", 33 | "protocol" => "HTTP/1.1", 34 | "sourceIp" => "72.218.219.201", 35 | "userAgent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15" 36 | }, 37 | "requestId" => "KSP7Mj94IAMEMFQ=", 38 | "routeKey" => "$default", 39 | "stage" => "production", 40 | "time" => "01/Apr/2020:02:18:09 +0000", 41 | "timeEpoch" => 1585707489142 42 | }, 43 | "isBase64Encoded" => false 44 | }.freeze 45 | 46 | # Via CloudFront directly to API Gateway w/Origin Path. 47 | # 48 | # {"version"=>"2.0", "routeKey"=>"$default", "rawPath"=>"/production/", "rawQueryString"=>"colors[]=blue&colors[]=red", "headers"=>{"accept"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "accept-encoding"=>"gzip", "content-length"=>"0", "host"=>"n12pmpajak.execute-api.us-east-1.amazonaws.com", "user-agent"=>"Amazon CloudFront", "via"=>"2.0 2f66aa06710fece8ed203ab0ea81eb56.cloudfront.net (CloudFront)", "x-amz-cf-id"=>"rQOlQ0dg3A9zrTfLqgBRWS8tDQJVmxtm3QrCf0wN0jEczCNucdJqKw==", "x-amzn-trace-id"=>"Root=1-5e83c9c2-b01dc280b06a4d00bcaeb480", "x-forwarded-for"=>"72.218.219.201, 3.231.2.1", "x-forwarded-host"=>"myawesomelambda.example.com", "x-forwarded-port"=>"443", "x-forwarded-proto"=>"https"}, "queryStringParameters"=>{"colors[]"=>"blue,red"}, "requestContext"=>{"accountId"=>nil, "apiId"=>"n12pmpajak", "domainName"=>"n12pmpajak.execute-api.us-east-1.amazonaws.com", "domainPrefix"=>"n12pmpajak", "http"=>{"method"=>"GET", "path"=>"/production/", "protocol"=>"HTTP/1.1", "sourceIp"=>" 3.231.2.1", "userAgent"=>"Amazon CloudFront"}, "requestId"=>"KRx2bgOfoAMETBA=", "routeKey"=>"$default", "stage"=>"production", "time"=>"31/Mar/2020:22:52:50 +0000", "timeEpoch"=>1585695170625}, "isBase64Encoded"=>false} 49 | 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper/events/http_v2_post.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class HttpV2Post < Base 4 | # Via Custom Domain Name integration. 5 | # 6 | self.event = { 7 | "version" => "2.0", 8 | "routeKey" => "$default", 9 | "rawPath" => "/production/login", 10 | "rawQueryString" => "", 11 | "cookies" => ["signal1=test", "signal2=control"], 12 | "headers" => { 13 | "accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 14 | "accept-encoding" => "gzip, deflate, br", 15 | "accept-language" => "en-us", 16 | "content-length" => "146", 17 | "content-type" => "application/x-www-form-urlencoded", 18 | "host" => "myawesomelambda.example.com", 19 | "origin" => "https://myawesomelambda.example.com", 20 | "referer" => "https://myawesomelambda.example.com/?colors[]=blue&colors[]=red", 21 | "user-agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15", 22 | "x-amzn-trace-id" => "Root=1-5e852ca4-3a8dd0c91bfb6e85f0dc55ba", 23 | "x-forwarded-for" => "72.218.219.201", 24 | "x-forwarded-port" => "443", 25 | "x-forwarded-proto" => "https" 26 | }, 27 | "requestContext" => { 28 | "accountId" => nil, 29 | "apiId" => "n12pmpajak", 30 | "domainName" => "myawesomelambda.example.com", 31 | "domainPrefix" => "myawesomelambda", 32 | "http" => { 33 | "method" => "POST", 34 | "path" => "/production/login", 35 | "protocol" => "HTTP/1.1", 36 | "sourceIp" => "72.218.219.201", 37 | "userAgent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15" 38 | }, 39 | "requestId" => "KVPpsgCooAMEMBQ=", 40 | "routeKey" => "$default", 41 | "stage" => "production", 42 | "time" => "02/Apr/2020:00:07:00 +0000", 43 | "timeEpoch" => 1585786020373 44 | }, 45 | "body" => "YXV0aGVudGljaXR5X3Rva2VuPThHR3ZFTTYlMkZiandERzNYQTNjaklaQk5Vc1RudWdTNXBjNEd1ZHlwNVpuZWNvcFFSNTYwcEt2bmJOcmhMYTRpVWpGT0NZT214JTJGeUV5SXBKcHNkM3NHQSUzRCUzRCZwYXNzd29yZD1wYXNzd29yZCZjb21taXQ9TG9naW4=", 46 | "isBase64Encoded" => true 47 | } 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /test/test_helper/events/rest.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class Rest < Base 4 | self.event = { 5 | "resource" => "/", 6 | "path" => "/", 7 | "httpMethod" => "GET", 8 | "headers" => { 9 | "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 10 | "Accept-Encoding" => "gzip", 11 | "Cookie" => "signal1=test; signal2=control", 12 | "Host" => "4o8v9z4feh.execute-api.us-east-1.amazonaws.com", 13 | "origin" => "https://myawesomelambda.example.com", 14 | "User-Agent" => "Amazon CloudFront", 15 | "Via" => "2.0 7f7e359e1c06a914d3d305785359b84d.cloudfront.net (CloudFront)", 16 | "X-Amz-Cf-Id" => "kXZzJ72NOsZSsPu-JzNUGyFei1G0r9uzoup3yHrwk4J5qGLKrdUrRA==", 17 | "X-Amzn-Trace-Id" => "Root=1-5e7fe714-fee6909429159440eb352c40", 18 | "X-Forwarded-For" => "72.218.219.201, 34.195.252.119", 19 | "X-Forwarded-Host" => "myawesomelambda.example.com", 20 | "X-Forwarded-Port" => "443", 21 | "X-Forwarded-Proto" => "https" 22 | }, 23 | "multiValueHeaders" => { 24 | "Accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 25 | "Accept-Encoding" => ["gzip"], 26 | "Cookie" => ["signal1=test; signal2=control"], 27 | "Host" => ["4o8v9z4feh.execute-api.us-east-1.amazonaws.com"], 28 | "origin" => ["https://myawesomelambda.example.com"], 29 | "User-Agent" => ["Amazon CloudFront"], 30 | "Via" => ["2.0 7f7e359e1c06a914d3d305785359b84d.cloudfront.net (CloudFront)"], 31 | "X-Amz-Cf-Id" => ["kXZzJ72NOsZSsPu-JzNUGyFei1G0r9uzoup3yHrwk4J5qGLKrdUrRA=="], 32 | "X-Amzn-Trace-Id" => ["Root=1-5e7fe714-fee6909429159440eb352c40"], 33 | "X-Forwarded-For" => ["72.218.219.201, 34.195.252.119"], 34 | "X-Forwarded-Host" => ["myawesomelambda.example.com"], 35 | "X-Forwarded-Port" => ["443"], 36 | "X-Forwarded-Proto" => ["https"] 37 | }, 38 | "queryStringParameters" => { "colors[]" => "red" }, 39 | "multiValueQueryStringParameters" => { "colors[]" => ["blue", "red"] }, 40 | "pathParameters" => nil, 41 | "stageVariables" => nil, 42 | "requestContext" => { 43 | "resourceId" => "77ce0rz741", 44 | "resourcePath" => "/", 45 | "httpMethod" => "GET", 46 | "extendedRequestId" => "KIELPF7PoAMFePQ=", 47 | "requestTime" => "29/Mar/2020:00:08:52 +0000", 48 | "path" => "/production/", 49 | "accountId" => nil, 50 | "protocol" => "HTTP/1.1", 51 | "stage" => "production", 52 | "domainPrefix" => "4o8v9z4feh", 53 | "requestTimeEpoch" => 1585440532675, 54 | "requestId" => "e5c78607-6a25-4f15-bb87-aca7d4522093", 55 | "identity" => { 56 | "cognitoIdentityPoolId" => nil, 57 | "accountId" => nil, 58 | "cognitoIdentityId" => nil, 59 | "caller" => nil, 60 | "sourceIp" => "72.218.219.201", 61 | "principalOrgId" => nil, 62 | "accessKey" => nil, 63 | "cognitoAuthenticationType" => nil, 64 | "cognitoAuthenticationProvider" => nil, 65 | "userArn" => nil, 66 | "userAgent" => "Amazon CloudFront", 67 | "user" => nil 68 | }, 69 | "domainName" => "4o8v9z4feh.execute-api.us-east-1.amazonaws.com", 70 | "apiId" => "4o8v9z4feh" 71 | }, 72 | "body" => nil, 73 | "isBase64Encoded" => false 74 | }.freeze 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /test/test_helper/events/rest_post.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Events 3 | class RestPost < Base 4 | 5 | self.event = { 6 | "resource" => "/{resource+}", 7 | "path" => "/login", 8 | "httpMethod" => "POST", 9 | "headers" => { 10 | "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 11 | "Accept-Encoding" => "gzip, deflate, br", 12 | "content-type" => "application/x-www-form-urlencoded", 13 | "Cookie" => "signal1=test; signal2=control", 14 | "Host" => "4o8v9z4feh.execute-api.us-east-1.amazonaws.com", 15 | "origin" => "https://myawesomelambda.example.com", 16 | "User-Agent" => "Amazon CloudFront", 17 | "Via" => "2.0 7f7e359e1c06a914d3d305785359b84d.cloudfront.net (CloudFront)", 18 | "X-Amz-Cf-Id" => "HlVPz9T-9eYwLqzFi21O7EU7b_dvHEzqlgs4YLetEq036kBnyNv6_Q==", 19 | "X-Amzn-Trace-Id" => "Root=1-5e7fe714-d306a3db0536f45b4197aa52", 20 | "X-Forwarded-For" => "72.218.219.201, 70.132.33.68", 21 | "X-Forwarded-Host" => "myawesomelambda.example.com", 22 | "X-Forwarded-Port" => "443", 23 | "X-Forwarded-Proto" => "https" 24 | }, 25 | "multiValueHeaders" => { 26 | "Accept" => ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], 27 | "Accept-Encoding" => ["gzip, deflate, br"], 28 | "content-type" => ["application/x-www-form-urlencoded"], 29 | "Cookie" => ["signal1=test; signal2=control"], 30 | "Host" => ["4o8v9z4feh.execute-api.us-east-1.amazonaws.com"], 31 | "origin" => ["https://myawesomelambda.example.com"], 32 | "User-Agent" => ["Amazon CloudFront"], 33 | "Via" => ["2.0 7f7e359e1c06a914d3d305785359b84d.cloudfront.net (CloudFront)"], 34 | "X-Amz-Cf-Id" => ["HlVPz9T-9eYwLqzFi21O7EU7b_dvHEzqlgs4YLetEq036kBnyNv6_Q=="], 35 | "X-Amzn-Trace-Id" => ["Root=1-5e7fe714-d306a3db0536f45b4197aa52"], 36 | "X-Forwarded-For" => ["72.218.219.201, 70.132.33.68"], 37 | "X-Forwarded-Host" => ["myawesomelambda.example.com"], 38 | "X-Forwarded-Port" => ["443"], 39 | "X-Forwarded-Proto" => ["https"] 40 | }, 41 | "queryStringParameters" => nil, 42 | "multiValueQueryStringParameters" => nil, 43 | "pathParameters" => { "resource" => "login" }, 44 | "stageVariables" => nil, 45 | "requestContext" => { 46 | "resourceId" => "s2mq69", 47 | "resourcePath" => "/{resource+}", 48 | "httpMethod" => "POST", 49 | "extendedRequestId" => "KIELOHygoAMFXxA=", 50 | "requestTime" => "29/Mar/2020:00:08:52 +0000", 51 | "path" => "/production/login", 52 | "accountId" => nil, 53 | "protocol" => "HTTP/1.1", 54 | "stage" => "production", 55 | "domainPrefix" => "4o8v9z4feh", 56 | "requestTimeEpoch" => 1585440532566, 57 | "requestId" => "552c9b8d-23fa-42de-9f89-9b56af1e6770", 58 | "identity" => { 59 | "cognitoIdentityPoolId" => nil, 60 | "accountId" => nil, 61 | "cognitoIdentityId" => nil, 62 | "caller" => nil, 63 | "sourceIp" => "72.218.219.201", 64 | "principalOrgId" => nil, 65 | "accessKey" => nil, 66 | "cognitoAuthenticationType" => nil, 67 | "cognitoAuthenticationProvider" => nil, 68 | "userArn" => nil, 69 | "userAgent" => "Amazon CloudFront", 70 | "user" => nil 71 | }, 72 | "domainName" => "4o8v9z4feh.execute-api.us-east-1.amazonaws.com", 73 | "apiId" => "4o8v9z4feh" 74 | }, 75 | "body" => "YXV0aGVudGljaXR5X3Rva2VuPXRObXZ0T2xlaHY0YU1GUlFlZTg4c2MxcTViZ3lJaGxqM3pscVJPelg5bWtyYnZwOVZrOUdoUlh4NG9sNFl4dW9jbDRJR2ZOOTk1ckh3SXhDSXhpVWZnJTNEJTNEJnBhc3N3b3JkPXBhc3N3b3JkJmNvbW1pdD1Mb2dpbg==", 76 | "isBase64Encoded" => true 77 | }.freeze 78 | 79 | end 80 | end 81 | end -------------------------------------------------------------------------------- /test/test_helper/jobs_helpers.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module Jobs 3 | class BasicJob < ActiveJob::Base 4 | def perform(object) 5 | puts "BasicJob with: #{object.inspect}" 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/test_helper/lambda_context.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | class LambdaContext 3 | 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 | 20 | def clock_diff 21 | 1585237646907 22 | end 23 | 24 | def deadline_ms 25 | 1585238452698 26 | end 27 | 28 | def aws_request_id 29 | 'a59284fd-d48c-4de5-af9e-df4254489ac2' 30 | end 31 | 32 | def invoked_function_arn 33 | 'arn:aws:lambda:us-east-1:123456789012:function:myawesomelambda' 34 | end 35 | 36 | def log_stream_name 37 | '2020/03/26[$LATEST]88b3605521bf4d7abfaa7bfa6dcd45f1' 38 | end 39 | 40 | def function_name 41 | 'myawesomelambda' 42 | end 43 | 44 | def memory_limit_in_mb 45 | '512' 46 | end 47 | 48 | def function_version 49 | '$LATEST' 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_helper/lambdakiq_helpers.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module LambdakiqHelpers 3 | 4 | private 5 | 6 | def lambdakiq_client 7 | Lambdakiq.client.sqs 8 | end 9 | 10 | def lambdakiq_client_reset! 11 | Lambdakiq.instance_variable_set :@client, nil 12 | end 13 | 14 | def lambdakiq_client_stub_responses 15 | lambdakiq_client.stub_responses(:get_queue_url, { 16 | queue_url: 'https://sqs.us-stubbed-1.amazonaws.com' 17 | }) 18 | redrive_policy = JSON.dump({maxReceiveCount: 8}) 19 | lambdakiq_client.stub_responses(:get_queue_attributes, { 20 | attributes: { 'RedrivePolicy' => redrive_policy } 21 | }) 22 | end 23 | 24 | def lambdakiq_api_requests 25 | lambdakiq_client.api_requests 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/test_helper/stream_helpers.rb: -------------------------------------------------------------------------------- 1 | module TestHelpers 2 | module StreamHelpers 3 | 4 | private 5 | 6 | def silence_stream(stream) 7 | old_stream = stream.dup 8 | stream.reopen(IO::NULL) 9 | stream.sync = true 10 | yield 11 | ensure 12 | stream.reopen(old_stream) 13 | old_stream.close 14 | end 15 | 16 | def quietly 17 | silence_stream(STDOUT) do 18 | silence_stream(STDERR) do 19 | yield 20 | end 21 | end 22 | end 23 | 24 | def capture(stream) 25 | stream = stream.to_s 26 | captured_stream = Tempfile.new(stream) 27 | stream_io = eval("$#{stream}") 28 | origin_stream = stream_io.dup 29 | stream_io.reopen(captured_stream) 30 | yield 31 | stream_io.rewind 32 | return captured_stream.read 33 | ensure 34 | captured_stream.close 35 | captured_stream.unlink 36 | stream_io.reopen(origin_stream) 37 | end 38 | 39 | end 40 | end 41 | --------------------------------------------------------------------------------