diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml new file mode 100644 index 00000000..81f3c655 --- /dev/null +++ b/.github/workflows/coverage.yaml @@ -0,0 +1,57 @@ +name: Coverage + +on: [push, pull_request] + +permissions: + contents: read + +env: + CONSOLE_OUTPUT: XTerm + COVERAGE: PartialSummary + +jobs: + test: + name: ${{matrix.ruby}} on ${{matrix.os}} + runs-on: ${{matrix.os}}-latest + + strategy: + matrix: + os: + - ubuntu + - macos + + ruby: + - "3.2" + + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - name: Run tests + timeout-minutes: 5 + run: bundle exec bake test + + - uses: actions/upload-artifact@v2 + with: + name: coverage-${{matrix.os}}-${{matrix.ruby}} + path: .covered.db + + validate: + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: true + + - uses: actions/download-artifact@v3 + + - name: Validate coverage + timeout-minutes: 5 + run: bundle exec bake covered:validate --paths */.covered.db \; diff --git a/.github/workflows/test-async-head.yaml b/.github/workflows/test-async-head.yaml deleted file mode 100644 index a6da4873..00000000 --- a/.github/workflows/test-async-head.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test Async HEAD - -on: [push, pull_request] - -permissions: - contents: read - -env: - CONSOLE_OUTPUT: XTerm - BUNDLE_GEMFILE: gems/async-head.rb - -jobs: - test: - runs-on: ${{matrix.os}}-latest - - strategy: - matrix: - os: - - ubuntu - - ruby: - - head - - steps: - - uses: actions/checkout@v3 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{matrix.ruby}} - bundler-cache: true - - - name: Run tests - timeout-minutes: 10 - run: bundle exec bake test diff --git a/.github/workflows/test-async-v1.yaml b/.github/workflows/test-async-v1.yaml deleted file mode 100644 index 9d83bd0b..00000000 --- a/.github/workflows/test-async-v1.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test Async v1 - -on: [push, pull_request] - -permissions: - contents: read - -env: - CONSOLE_OUTPUT: XTerm - BUNDLE_GEMFILE: gems/async-v1.rb - -jobs: - test: - runs-on: ${{matrix.os}}-latest - - strategy: - matrix: - os: - - ubuntu - - ruby: - - 2.7 - - steps: - - uses: actions/checkout@v3 - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{matrix.ruby}} - bundler-cache: true - - - name: Run tests - timeout-minutes: 10 - run: bundle exec bake test diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml index 214149c4..cbff6759 100644 --- a/.github/workflows/test-external.yaml +++ b/.github/workflows/test-external.yaml @@ -20,7 +20,6 @@ jobs: - macos ruby: - - "2.7" - "3.0" - "3.1" - "3.2" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a6b57f1..942ede73 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,7 +21,6 @@ jobs: - macos ruby: - - "2.7" - "3.0" - "3.1" - "3.2" @@ -46,10 +45,6 @@ jobs: ruby-version: ${{matrix.ruby}} bundler-cache: true - - name: Installing packages (ubuntu) - if: matrix.os == 'ubuntu' - run: sudo apt-get install apache2-utils - - name: Run tests timeout-minutes: 10 run: bundle exec bake test diff --git a/.gitignore b/.gitignore index 4da16e76..09a72e06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -.tags - /.bundle/ -/.yardoc -/gems.locked -/_yardoc/ -/coverage/ -/doc/ /pkg/ -/spec/reports/ -/tmp/ - -.rspec_status -.covered.db -/h2spec +/gems.locked +/.covered.db +/external diff --git a/.rspec_status b/.rspec_status new file mode 100644 index 00000000..4e8c50e8 --- /dev/null +++ b/.rspec_status @@ -0,0 +1,202 @@ +example_id | status | run_time | +------------------------------------------------------ | ------ | --------------- | +./spec/async/http/body/hijack_spec.rb[1:1:1] | passed | 0.01608 seconds | +./spec/async/http/body/hijack_spec.rb[1:1:2] | passed | 0.01184 seconds | +./spec/async/http/body/pipe_spec.rb[1:1:1:1] | passed | 0.02183 seconds | +./spec/async/http/body/pipe_spec.rb[1:1:2:1] | passed | 0.01143 seconds | +./spec/async/http/body/pipe_spec.rb[1:2:1:1] | passed | 0.00014 seconds | +./spec/async/http/body/pipe_spec.rb[1:2:2:1] | passed | 0.00011 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:1] | passed | 0.01125 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:2] | passed | 0.01119 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:3:1] | passed | 0.01121 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:4:1] | passed | 0.01142 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:4:2] | passed | 0.01109 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:4:3] | passed | 0.01111 seconds | +./spec/async/http/body/slowloris_spec.rb[1:1:4:4] | passed | 0.01112 seconds | +./spec/async/http/body/slowloris_spec.rb[1:2] | passed | 0.11263 seconds | +./spec/async/http/body/slowloris_spec.rb[1:3] | passed | 0.01134 seconds | +./spec/async/http/body/writable_spec.rb[1:1:1] | passed | 0.01112 seconds | +./spec/async/http/body/writable_spec.rb[1:1:2] | passed | 0.01095 seconds | +./spec/async/http/body/writable_spec.rb[1:1:3:1] | passed | 0.01113 seconds | +./spec/async/http/body/writable_spec.rb[1:1:4:1] | passed | 0.01105 seconds | +./spec/async/http/body/writable_spec.rb[1:1:4:2] | passed | 0.01111 seconds | +./spec/async/http/body/writable_spec.rb[1:1:4:3] | passed | 0.01109 seconds | +./spec/async/http/body/writable_spec.rb[1:1:4:4] | passed | 0.01103 seconds | +./spec/async/http/body_spec.rb[1:1:1] | passed | 0.01176 seconds | +./spec/async/http/body_spec.rb[1:1:2] | passed | 0.01165 seconds | +./spec/async/http/body_spec.rb[2:1:1] | passed | 0.16983 seconds | +./spec/async/http/body_spec.rb[2:1:2] | passed | 0.07835 seconds | +./spec/async/http/client/google_spec.rb[1:1] | passed | 0.55517 seconds | +./spec/async/http/client_spec.rb[1:1:1] | passed | 0.01671 seconds | +./spec/async/http/client_spec.rb[1:2:1] | passed | 0.0268 seconds | +./spec/async/http/client_spec.rb[1:3:1] | passed | 0.01327 seconds | +./spec/async/http/client_spec.rb[1:3:2] | passed | 1.44 seconds | +./spec/async/http/client_spec.rb[1:3:3] | passed | 1.08 seconds | +./spec/async/http/endpoint_spec.rb[1:1] | passed | 0.00008 seconds | +./spec/async/http/endpoint_spec.rb[1:2:1] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:2:2] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:3:1:1] | passed | 0.00079 seconds | +./spec/async/http/endpoint_spec.rb[1:3:1:2] | passed | 0.00019 seconds | +./spec/async/http/endpoint_spec.rb[1:3:2:1] | passed | 0.00005 seconds | +./spec/async/http/endpoint_spec.rb[1:3:2:2] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:3:2:3] | passed | 0.00014 seconds | +./spec/async/http/endpoint_spec.rb[1:4:1:1] | passed | 0.00007 seconds | +./spec/async/http/endpoint_spec.rb[1:4:1:2] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:4:2:1] | passed | 0.00006 seconds | +./spec/async/http/endpoint_spec.rb[1:5:1:1] | passed | 0.00005 seconds | +./spec/async/http/endpoint_spec.rb[1:5:2:1] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:5:3:1] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:6:1:1] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:6:2:1] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:6:3:1] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:6:4:1] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:6:5:1] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[1:7:1] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[1:7:2] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[2:1] | passed | 0.00003 seconds | +./spec/async/http/endpoint_spec.rb[2:2] | passed | 0.00022 seconds | +./spec/async/http/endpoint_spec.rb[2:3] | passed | 0.00004 seconds | +./spec/async/http/endpoint_spec.rb[2:4] | passed | 0.00005 seconds | +./spec/async/http/internet/instance_spec.rb[1:1:1] | passed | 0.00003 seconds | +./spec/async/http/internet_spec.rb[1:1] | passed | 1.08 seconds | +./spec/async/http/internet_spec.rb[1:2] | passed | 1.22 seconds | +./spec/async/http/performance_spec.rb[1:1:1] | passed | 0.04966 seconds | +./spec/async/http/performance_spec.rb[1:2:1] | passed | 0.04596 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:1] | passed | 0.0231 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:2:1] | passed | 0.01434 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:3:1] | passed | 0.43318 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:4:1] | passed | 0.01564 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:5:1] | passed | 0.01386 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:6:1] | passed | 0.01293 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:6:2] | passed | 1.02 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:1] | passed | 0.01514 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:2] | passed | 1.02 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:1] | passed | 0.14361 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:1] | passed | 0.01789 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:2] | passed | 0.01502 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:3] | passed | 0.01514 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:4] | passed | 0.01436 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:5] | passed | 0.01428 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:3:2:6] | passed | 0.01469 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:4:1] | passed | 0.01485 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:5:1] | passed | 0.01469 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:5:2] | passed | 0.01472 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:5:3] | passed | 0.01573 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:7:5:4] | passed | 0.01485 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:8:1] | passed | 0.01395 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:9:1] | passed | 0.01565 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:10:1] | passed | 0.01424 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:11:1] | passed | 0.01361 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:12:1] | passed | 0.01405 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:13:1] | passed | 0.01366 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:14:1] | passed | 0.01363 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:15:1] | passed | 0.11588 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:16:1] | passed | 0.23731 seconds | +./spec/async/http/protocol/http10_spec.rb[1:1:17:1] | passed | 0.23891 seconds | +./spec/async/http/protocol/http11/desync_spec.rb[1:1] | passed | 3.39 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:1] | passed | 0.01789 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:2:1] | passed | 0.01501 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:3:1] | passed | 0.41109 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:4:1] | passed | 0.01665 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:5:1] | passed | 0.0155 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:6:1] | passed | 0.02588 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:6:2] | passed | 0.02548 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:1] | passed | 0.0148 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:2] | passed | 1.02 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:1] | passed | 0.09381 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:1] | passed | 0.01818 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:2] | passed | 0.01732 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:3] | passed | 0.01566 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:4] | passed | 0.01533 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:5] | passed | 0.01522 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:3:2:6] | passed | 0.01497 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:4:1] | passed | 0.01502 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:5:1] | passed | 0.01535 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:5:2] | passed | 0.01506 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:5:3] | passed | 0.01473 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:7:5:4] | passed | 0.01475 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:8:1] | passed | 0.01481 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:9:1] | passed | 0.01477 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:10:1] | passed | 0.01535 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:11:1] | passed | 0.01514 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:12:1] | passed | 0.01478 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:13:1] | passed | 0.01462 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:14:1] | passed | 0.01575 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:15:1] | passed | 0.11794 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:16:1] | passed | 0.62467 seconds | +./spec/async/http/protocol/http11_spec.rb[1:1:17:1] | passed | 0.24901 seconds | +./spec/async/http/protocol/http11_spec.rb[1:2:1] | passed | 0.01756 seconds | +./spec/async/http/protocol/http11_spec.rb[1:3:1] | passed | 0.01517 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:1] | passed | 0.01722 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:2:1] | passed | 0.01595 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:3:1] | passed | 3.49 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:4:1] | passed | 0.01817 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:5:1] | passed | 0.01483 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:6:1] | passed | 0.02516 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:6:2] | passed | 0.0253 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:1] | passed | 0.01426 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:2] | passed | 1.02 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:1] | passed | 0.19562 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:1] | passed | 0.01771 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:2] | passed | 0.01559 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:3] | passed | 0.01516 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:4] | passed | 0.01512 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:5] | passed | 0.01476 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:3:2:6] | passed | 0.01483 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:4:1] | passed | 0.01444 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:5:1] | passed | 0.0146 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:5:2] | passed | 0.01455 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:5:3] | passed | 0.01473 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:7:5:4] | passed | 0.01497 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:8:1] | passed | 0.01522 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:9:1] | passed | 0.0145 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:10:1] | passed | 0.01435 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:11:1] | passed | 0.01489 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:12:1] | passed | 0.01453 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:13:1] | passed | 0.01439 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:14:1] | passed | 0.01443 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:15:1] | passed | 0.1152 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:16:1] | passed | 0.62365 seconds | +./spec/async/http/protocol/http2_spec.rb[1:1:17:1] | passed | 0.23778 seconds | +./spec/async/http/protocol/http2_spec.rb[1:2:1] | passed | 0.01744 seconds | +./spec/async/http/protocol/http2_spec.rb[1:3:1] | passed | 0.01594 seconds | +./spec/async/http/protocol/http2_spec.rb[1:4:1] | passed | 0.01527 seconds | +./spec/async/http/protocol/http2_spec.rb[1:5:1] | passed | 0.03528 seconds | +./spec/async/http/proxy_spec.rb[1:1:1:1] | passed | 0.01431 seconds | +./spec/async/http/proxy_spec.rb[1:1:2:1] | passed | 0.01419 seconds | +./spec/async/http/proxy_spec.rb[1:1:3:1] | passed | 0.0147 seconds | +./spec/async/http/proxy_spec.rb[1:1:4:1] | passed | 0.01478 seconds | +./spec/async/http/proxy_spec.rb[1:1:4:2] | passed | 0.01475 seconds | +./spec/async/http/proxy_spec.rb[1:1:5:1] | passed | 0.46421 seconds | +./spec/async/http/proxy_spec.rb[1:1:5:2] | passed | 0.65114 seconds | +./spec/async/http/proxy_spec.rb[1:1:5:3:1:1] | passed | 0.6387 seconds | +./spec/async/http/proxy_spec.rb[1:1:5:3:2:1] | passed | 0.02658 seconds | +./spec/async/http/proxy_spec.rb[2:1:1:1] | passed | 0.01853 seconds | +./spec/async/http/proxy_spec.rb[2:1:2:1] | passed | 0.01437 seconds | +./spec/async/http/proxy_spec.rb[2:1:3:1] | passed | 0.01446 seconds | +./spec/async/http/proxy_spec.rb[2:1:4:1] | passed | 0.01431 seconds | +./spec/async/http/proxy_spec.rb[2:1:4:2] | passed | 0.01418 seconds | +./spec/async/http/proxy_spec.rb[2:1:5:1] | passed | 0.46969 seconds | +./spec/async/http/proxy_spec.rb[2:1:5:2] | passed | 0.63947 seconds | +./spec/async/http/proxy_spec.rb[2:1:5:3:1:1] | passed | 0.65739 seconds | +./spec/async/http/proxy_spec.rb[2:1:5:3:2:1] | passed | 0.02528 seconds | +./spec/async/http/proxy_spec.rb[3:1:1:1] | passed | 0.0165 seconds | +./spec/async/http/proxy_spec.rb[3:1:2:1] | passed | 0.01401 seconds | +./spec/async/http/proxy_spec.rb[3:1:3:1] | passed | 0.01474 seconds | +./spec/async/http/proxy_spec.rb[3:1:4:1] | passed | 0.01485 seconds | +./spec/async/http/proxy_spec.rb[3:1:4:2] | passed | 0.01475 seconds | +./spec/async/http/proxy_spec.rb[3:1:5:1] | passed | 0.47084 seconds | +./spec/async/http/proxy_spec.rb[3:1:5:2] | passed | 0.66425 seconds | +./spec/async/http/proxy_spec.rb[3:1:5:3:1:1] | passed | 0.66628 seconds | +./spec/async/http/proxy_spec.rb[3:1:5:3:2:1] | passed | 0.0256 seconds | +./spec/async/http/relative_location_spec.rb[1:1:1:1] | passed | 0.01881 seconds | +./spec/async/http/relative_location_spec.rb[1:1:1:2:1] | passed | 0.01429 seconds | +./spec/async/http/relative_location_spec.rb[1:1:1:2:2] | passed | 0.01463 seconds | +./spec/async/http/relative_location_spec.rb[1:1:2:1] | passed | 0.01435 seconds | +./spec/async/http/relative_location_spec.rb[1:1:3:1] | passed | 0.01423 seconds | +./spec/async/http/relative_location_spec.rb[1:1:4:1] | passed | 0.01419 seconds | +./spec/async/http/retry_spec.rb[1:1] | passed | 0.16652 seconds | +./spec/async/http/retry_spec.rb[1:2] | passed | 0.1711 seconds | +./spec/async/http/ssl_spec.rb[1:1:1] | passed | 0.24282 seconds | +./spec/async/http/statistics_spec.rb[1:1] | passed | 0.01526 seconds | +./spec/rack/test_spec.rb[1:1] | passed | 0.42049 seconds | diff --git a/async-http.gemspec b/async-http.gemspec index d1c579f5..0323abee 100644 --- a/async-http.gemspec +++ b/async-http.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |spec| spec.version = Async::HTTP::VERSION spec.summary = "A HTTP client and server library." - spec.authors = ["Samuel Williams", "Brian Morearty", "Bruno Sutic", "Janko Marohnić", "Adam Daniels", "Cyril Roelandt", "Denis Talakevich", "Ian Ker-Seymer", "Igor Sidorov", "Marco Concetto Rudilosso", "Olle Jonsson", "Orgad Shaneh", "Sam Shadwell", "Stefan Wrobel", "Tim Meusel", "Trevor Turk", "Viacheslav Koval"] + spec.authors = ["Samuel Williams", "Brian Morearty", "Bruno Sutic", "Janko Marohnić", "Adam Daniels", "Thomas Morgan", "Cyril Roelandt", "Denis Talakevich", "Ian Ker-Seymer", "Igor Sidorov", "Marco Concetto Rudilosso", "Olle Jonsson", "Orgad Shaneh", "Sam Shadwell", "Stefan Wrobel", "Tim Meusel", "Trevor Turk", "Viacheslav Koval"] spec.license = "MIT" spec.cert_chain = ['release.cert'] @@ -17,6 +17,8 @@ Gem::Specification.new do |spec| spec.files = Dir.glob(['{bake,lib}/**/*', '*.md'], File::FNM_DOTMATCH, base: __dir__) + spec.required_ruby_version = ">= 3.0" + spec.add_dependency "async", ">= 1.25" spec.add_dependency "async-io", ">= 1.28" spec.add_dependency "async-pool", ">= 0.2" @@ -24,11 +26,4 @@ Gem::Specification.new do |spec| spec.add_dependency "protocol-http1", "~> 0.15.0" spec.add_dependency "protocol-http2", "~> 0.15.0" spec.add_dependency "traces", ">= 0.10.0" - - spec.add_development_dependency "async-container", "~> 0.14" - spec.add_development_dependency "async-rspec", "~> 1.10" - spec.add_development_dependency "covered" - spec.add_development_dependency "localhost" - spec.add_development_dependency "rack-test" - spec.add_development_dependency "rspec", "~> 3.6" end diff --git a/config/external.yaml b/config/external.yaml index 663e3e16..d94b9192 100644 --- a/config/external.yaml +++ b/config/external.yaml @@ -1,6 +1,6 @@ falcon: url: https://github.com/socketry/falcon.git - command: bundle exec rspec + command: bundle exec bake test async-rest: url: https://github.com/socketry/async-rest.git command: bundle exec rspec diff --git a/config/sus.rb b/config/sus.rb new file mode 100644 index 00000000..ee30cfcf --- /dev/null +++ b/config/sus.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2017-2023, by Samuel Williams. +# Copyright, 2018, by Janko Marohnić. + +ENV['CONSOLE_LEVEL'] ||= 'fatal' + +require 'covered/sus' +include Covered::Sus + +require 'traces' +ENV['TRACES_BACKEND'] ||= 'traces/backend/test' diff --git a/examples/google/about.html b/examples/google/about.html new file mode 100644 index 00000000..4bf91836 --- /dev/null +++ b/examples/google/about.html @@ -0,0 +1,16 @@ +ruby - Google Search
Google
×

See results about

Founded in 2002, RUBY is an eminent fashion label based in Aotearoa, best known and loved for the mutually nourishing relationship it has with its community ...
RUBY. RUBETTES: A community who cares. RUBY, @liampatterns & @rubysaysrecycle, Aotearoa. www.rubynz.com. 7,526 posts. 89.9K followers. 426 following.
RUBY. 44321 likes · 286 talking about this. Welcome to the official RUBY & Liam Facebook page.
Shop for RUBY designer womens fashion online in New Zealand. Free NZ Shipping over $50. Stress-free Returns!
$30.00
100% New Zealand hazelnuts coated in Belgian ruby chocolate – the fourth category in chocolate after dark, milk and white. Ruby chocolate is made from the ...
$54.00
1 x Ruby Hazelnuts (200g) - 100% New Zealand hazelnuts coated in Belgian ruby chocolate – without adding any colourants or fruit flavourings, the unique taste ...
Taylors Fine Ruby Port 750ml · Description. Stylish fruity nose, full of intense concentrated blackcurrant and cherry aromas, full bodied palate crammed with ...
\ No newline at end of file diff --git a/examples/google/gems.locked b/examples/google/gems.locked new file mode 100644 index 00000000..7fa8531a --- /dev/null +++ b/examples/google/gems.locked @@ -0,0 +1,41 @@ +GEM + remote: https://rubygems.org/ + specs: + async (2.3.1) + console (~> 1.10) + io-event (~> 1.1) + timers (~> 4.1) + async-http (0.60.1) + async (>= 1.25) + async-io (>= 1.28) + async-pool (>= 0.2) + protocol-http (~> 0.24.0) + protocol-http1 (~> 0.15.0) + protocol-http2 (~> 0.15.0) + traces (>= 0.8.0) + async-io (1.34.3) + async + async-pool (0.3.12) + async (>= 1.25) + console (1.16.2) + fiber-local + fiber-local (1.0.0) + io-event (1.1.6) + protocol-hpack (1.4.2) + protocol-http (0.24.1) + protocol-http1 (0.15.0) + protocol-http (~> 0.22) + protocol-http2 (0.15.1) + protocol-hpack (~> 1.4) + protocol-http (~> 0.18) + timers (4.3.5) + traces (0.8.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + async-http (~> 0.60.0) + +BUNDLED WITH + 2.4.6 diff --git a/examples/google/gems.rb b/examples/google/gems.rb new file mode 100644 index 00000000..ce0a1bea --- /dev/null +++ b/examples/google/gems.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +source "https://rubygems.org" + +gem "async-http", "~> 0.60.0" diff --git a/examples/google/multiple.rb b/examples/google/multiple.rb new file mode 100755 index 00000000..ad9123f7 --- /dev/null +++ b/examples/google/multiple.rb @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'async' +require 'async/barrier' +require 'async/semaphore' +require 'async/http/internet' + +TOPICS = ["ruby", "python", "rust"] + +Async do + internet = Async::HTTP::Internet.new + barrier = Async::Barrier.new + semaphore = Async::Semaphore.new(2, parent: barrier) + + # Spawn an asynchronous task for each topic: + TOPICS.each do |topic| + semaphore.async do + response = internet.get "https://www.google.com/search?q=#{topic}" + puts "Found #{topic}: #{response.read.scan(topic).size} times." + end + end + + # Ensure we wait for all requests to complete before continuing: + barrier.wait +ensure + internet&.close +end diff --git a/examples/google/ruby.html b/examples/google/ruby.html new file mode 100644 index 00000000..e311ea4a --- /dev/null +++ b/examples/google/ruby.html @@ -0,0 +1,22 @@ +ruby - Google Search
Google
×
Founded in 2002, RUBY is an eminent fashion label based in Aotearoa, best known and loved for the mutually nourishing relationship it has with its community ...
RUBY. RUBETTES: A community who cares. RUBY, @liampatterns & @rubysaysrecycle, Aotearoa. www.rubynz.com. 7,526 posts. 89.9K followers. 426 following.
RUBY. 44321 likes · 286 talking about this. Welcome to the official RUBY & Liam Facebook page.
Shop for RUBY designer womens fashion online in New Zealand. Free NZ Shipping over $50. Stress-free Returns!
$249.00
Firebird Pant in Black by New Zealand RUBY High-waisted pant with flattering straight leg silhouette Waist band with invisible zip closure at centre back ...
$30.00
100% New Zealand hazelnuts coated in Belgian ruby chocolate – the fourth category in chocolate after dark, milk and white. Ruby chocolate is made from the ...
Taylors Fine Ruby Port 750ml · Description. Stylish fruity nose, full of intense concentrated blackcurrant and cherry aromas, full bodied palate crammed with ...
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\x3d';var i=['dimg_27'];_setImagesSrc(i,s);})(); \ No newline at end of file diff --git a/fixtures/async/http/a_protocol.rb b/fixtures/async/http/a_protocol.rb new file mode 100644 index 00000000..a151efea --- /dev/null +++ b/fixtures/async/http/a_protocol.rb @@ -0,0 +1,566 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. +# Copyright, 2020, by Igor Sidorov. + +require 'async' +require 'async/clock' +require 'async/http/client' +require 'async/http/server' +require 'async/http/endpoint' +require 'async/http/body/hijack' +require 'tempfile' + +require 'protocol/http/body/file' + +require 'sus/fixtures/async/http' + +module Async + module HTTP + AProtocol = Sus::Shared("a protocol") do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:protocol) {subject} + + it "should have valid scheme" do + expect(client.scheme).to be == "http" + end + + with '#close' do + it 'can close the connection' do + Async do |task| + response = client.get("/") + expect(response).to be(:success?) + response.finish + + client.close + + expect(task.children).to be(:empty?) + end.wait + end + end + + with "huge body", timeout: 600 do + let(:body) {::Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "client can download data quickly" do + response = client.get("/") + expect(response).to be(:success?) + + data_size = 0 + duration = Async::Clock.measure do + while chunk = response.body.read + data_size += chunk.bytesize + chunk.clear + end + + response.finish + end + + size_mbytes = data_size / 1024**2 + + inform "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" + end + end + + with 'buffered body' do + let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} + let(:response) {::Protocol::HTTP::Response[200, {}, body]} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + response + end + end + + it "response body should be closed" do + expect(body).to receive(:close) + # expect(response).to receive(:close) + + expect(client.get("/", {}).read).to be == "Hello World" + end + end + + with 'empty body' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[204] + end + end + + it 'properly handles no content responses' do + expect(client.get("/", {}).read).to be_nil + end + end + + with 'with trailer' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if trailer = request.headers['trailer'] + expect(request.headers).not.to have_keys('etag') + request.finish + expect(request.headers).to have_keys('etag') + + ::Protocol::HTTP::Response[200, [], "request trailer"] + else + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("response trailer") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + ::Protocol::HTTP::Response[200, headers, body] + end + end + end + + it "can send request trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + headers = ::Protocol::HTTP::Headers.new + headers.add('trailer', 'etag') + body = Async::HTTP::Body::Writable.new + + Async do |task| + body.write("Hello") + task.sleep(0.01) + headers.add('etag', 'abcd') + body.close + end + + response = client.post("/", headers, body) + expect(response.read).to be == "request trailer" + + expect(response).to be(:success?) + end + + it "can receive response trailer" do + skip "Protocol does not support trailers!" unless subject.bidirectional? + + response = client.get("/") + expect(response.headers).to have_keys('trailer') + headers = response.headers + expect(headers).not.to have_keys('etag') + + expect(response.read).to be == "response trailer" + expect(response).to be(:success?) + + # It was sent as a trailer. + expect(headers).to have_keys('etag') + end + end + + with 'with working server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.method == 'POST' + # We stream the request body directly to the response. + ::Protocol::HTTP::Response[200, {}, request.body] + elsif request.method == 'GET' + expect(request.body).to be_nil + + ::Protocol::HTTP::Response[200, { + 'remote-address' => request.remote_address.inspect + }, ["#{request.method} #{request.version}"]] + else + ::Protocol::HTTP::Response[200, {}, ["Hello World"]] + end + end + end + + it "should have valid scheme" do + expect(server.scheme).to be == "http" + end + + it "disconnects slow clients" do + response = client.get("/") + response.read + + # We expect this connection to be closed: + connection = response.connection + + reactor.sleep(1.0) + + response = client.get("/") + response.read + + expect(connection).not.to be(:reusable?) + + # client.close + # reactor.sleep(0.1) + # reactor.print_hierarchy + end + + with 'using GET method' do + let(:expected) {"GET #{protocol::VERSION}"} + + it "can handle many simultaneous requests" do + duration = Async::Clock.measure do + 10.times do + tasks = 100.times.collect do + Async do + client.get("/") + end + end + + tasks.each do |task| + response = task.wait + expect(response).to be(:success?) + expect(response.read).to be == expected + end + end + end + + inform "Pool: #{client.pool}" + inform "Duration: #{duration.round(2)}" + end + + with 'with response' do + let(:response) {client.get("/")} + + def after + response.finish + super + end + + it "can finish gracefully" do + expect(response).to be(:success?) + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == expected + end + + it "provides content length" do + expect(response.body.length).not.to be_nil + end + + let(:tempfile) {Tempfile.new} + + it "can save to disk" do + response.save(tempfile.path) + expect(tempfile.read).to be == expected + + tempfile.close + end + + it "has remote-address header" do + expect(response.headers['remote-address']).not.to be_nil + end + + it "has protocol version" do + expect(response.version).not.to be_nil + end + end + end + + with 'HEAD' do + let(:response) {client.head("/")} + + it "is successful and without body" do + expect(response).to be(:success?) + expect(response.body).not.to be_nil + expect(response.body).to be(:empty?) + expect(response.body.length).not.to be_nil + expect(response.read).to be_nil + end + end + + with 'POST' do + let(:response) {client.post("/", {}, ["Hello", " ", "World"])} + + def after + response.finish + super + end + + it "is successful" do + expect(response).to be(:success?) + expect(response.read).to be == "Hello World" + expect(client.pool).not.to be(:busy?) + end + + it "can buffer response" do + buffer = response.finish + + expect(buffer.join).to be == "Hello World" + + expect(client.pool).not.to be(:busy?) + end + + it "should not contain content-length response header" do + expect(response.headers).not.to have_keys('content-length') + end + + it "fails gracefully when closing connection" do + client.pool.acquire do |connection| + connection.stream.close + end + end + end + end + + with 'content length' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] + end + end + + it "can send push promises" do + response = client.post("/test", [], ["Hello World!"]) + expect(response).to be(:success?) + + expect(response.body.length).to be == 18 + expect(response.read).to be == "Content Length: 12" + end + end + + with 'hijack with nil response' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + nil + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect(response).to be(:server_failure?) + end + end + + with 'partial hijack' do + let(:content) {"Hello World!"} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| + stream.write content + stream.write content + stream.close + end + end + end + + it "reads hijacked body" do + response = client.get("/") + + expect(response.read).to be == (content*2) + end + end + + with 'body with incorrect length' do + let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} + + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, bad_body] + end + end + + it "fails with appropriate error" do + response = client.get("/") + + expect do + response.read + end.to raise_exception(EOFError) + end + end + + with 'streaming server' do + let(:sent_chunks) {[]} + + let(:app) do + chunks = sent_chunks + + ::Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + Async::Reactor.run do |task| + 10.times do |i| + chunk = "Chunk #{i}" + chunks << chunk + + body.write chunk + task.sleep 0.25 + end + + body.finish + end + + ::Protocol::HTTP::Response[200, {}, body] + end + end + + it "can cancel response" do + response = client.get("/") + + expect(response.body.read).to be == "Chunk 0" + + response.close + + expect(sent_chunks).to be == ["Chunk 0"] + end + end + + with 'hijack server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + if request.hijack? + io = request.hijack! + io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" + io.flush + io.close + else + ::Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] + end + end + end + + it "will hijack response if possible" do + response = client.get("/") + + expect(response.read).to be =~ /Hijack/ + end + end + + with 'broken server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + raise RuntimeError.new('simulated failure') + end + end + + it "can't get /" do + expect do + response = client.get("/") + end.to raise_exception(Exception) + end + end + + with 'slow server' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + sleep(endpoint.timeout * 2) + ::Protocol::HTTP::Response[200, {}, []] + end + end + + it "can't get /" do + expect do + client.get("/") + end.to raise_exception(Async::TimeoutError) + end + end + + with 'bi-directional streaming' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + # Echo the request body back to the client. + ::Protocol::HTTP::Response[200, {}, request.body] + end + end + + it "can read from request body and write response body simultaneously" do + skip "Protocol does not support bidirectional streaming!" unless subject.bidirectional? + + body = Async::HTTP::Body::Writable.new + + # Ideally, the flow here is as follows: + # 1/ Client writes headers to server. + # 2/ Client starts writing data to server (in async task). + # 3/ Client reads headers from server. + # 4a/ Client reads data from server. + # 4b/ Client finishes sending data to server. + response = client.post(endpoint.path, [], body) + + expect(response).to be(:success?) + + body.write "." + count = 0 + + response.each do |chunk| + if chunk.bytesize > 32 + body.close + else + count += 1 + body.write chunk*2 + Async::Task.current.sleep(0.1) + end + end + + expect(count).to be == 6 + end + end + + with 'multiple client requests' do + let(:app) do + ::Protocol::HTTP::Middleware.for do |request| + ::Protocol::HTTP::Response[200, {}, [request.path]] + end + end + + def around + current = Console.logger.level + Console.logger.fatal! + + super + ensure + Console.logger.level = current + end + + it "doesn't cancel all requests" do + tasks = [] + task = Async::Task.current + stopped = [] + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/a').finish + end + ensure + stopped << 'a' + end + } + end + + 10.times do + tasks << task.async { + begin + loop do + client.get('http://127.0.0.1:8080/b').finish + end + ensure + stopped << 'b' + end + } + end + + tasks.each do |child| + task.sleep 0.01 + child.stop + end + + expect(stopped.sort).to be == stopped + end + end + end + end +end diff --git a/fixtures/async/http/body/a_writable_body.rb b/fixtures/async/http/body/a_writable_body.rb new file mode 100644 index 00000000..eca1c5d9 --- /dev/null +++ b/fixtures/async/http/body/a_writable_body.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2023, by Samuel Williams. + +require 'protocol/http/body/deflate' + +module Async + module HTTP + module Body + AWritableBody = Sus::Shared("a writable body") do + it "can write and read data" do + 3.times do |i| + body.write("Hello World #{i}") + expect(body.read).to be == "Hello World #{i}" + end + end + + it "can buffer data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + 3.times do |i| + expect(body.read).to be == "Hello World #{i}" + end + end + + with '#join' do + it "can join chunks" do + 3.times do |i| + body.write("#{i}") + end + + body.close + + expect(body.join).to be == "012" + end + end + + with '#each' do + it "can read all data in order" do + 3.times do |i| + body.write("Hello World #{i}") + end + + body.close + + 3.times do |i| + chunk = body.read + expect(chunk).to be == "Hello World #{i}" + end + end + + it "can propagate failures" do + reactor.async do + expect do + body.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "can propagate failures in nested bodies" do + nested = ::Protocol::HTTP::Body::Deflate.for(body) + + reactor.async do + expect do + nested.each do |chunk| + raise RuntimeError.new("It was too big!") + end + end.to raise_exception(RuntimeError, message: be =~ /big/) + end + + expect{ + body.write("Beep boop") # This will cause a failure. + ::Async::Task.current.yield + body.write("Beep boop") # This will fail. + }.to raise_exception(RuntimeError, message: be =~ /big/) + end + + it "will stop after finishing" do + output_task = reactor.async do + body.each do |chunk| + expect(chunk).to be == "Hello World!" + end + end + + body.write("Hello World!") + body.close + + expect(body).not.to be(:empty?) + + ::Async::Task.current.yield + + expect(output_task).to be(:finished?) + expect(body).to be(:empty?) + end + end + end + end + end +end diff --git a/gems.rb b/gems.rb index 435688d1..2173f993 100644 --- a/gems.rb +++ b/gems.rb @@ -7,6 +7,15 @@ gemspec +# gem "async", path: "../async" +# gem "async-io", path: "../async-io" +# gem "traces", path: "../traces" + +# gem "protocol-http", path: "../protocol-http" +# gem "protocol-http1", path: "../protocol-http1" +# gem "protocol-http2", path: "../protocol-http2" +# gem "protocol-hpack", path: "../protocol-hpack" + group :maintenance, optional: true do gem "bake-modernize" gem "bake-gem" @@ -16,18 +25,22 @@ end group :test do + gem "covered" + gem "sus" + gem "sus-fixtures-async" + gem "sus-fixtures-async-http", "~> 0.7" + gem "sus-fixtures-openssl" + gem "bake" gem "bake-test" gem "bake-test-external" -end - -# gem "async", path: "../async" -# gem "async-io", path: "../async-io" -# gem "traces", path: "../traces" - -# gem "protocol-http", path: "../protocol-http" -# gem "protocol-http1", path: "../protocol-http1" -# gem "protocol-http2", path: "../protocol-http2" -# gem "protocol-hpack", path: "../protocol-hpack" + + gem "async-container", "~> 0.14" + gem "async-rspec", "~> 1.10" -gem "thread-local" + gem "localhost" + gem "rack-test" + + # Optional dependency: + gem "thread-local" +end diff --git a/lib/async/http/body/delayed.rb b/lib/async/http/body/delayed.rb index 95675809..7b0f57b5 100644 --- a/lib/async/http/body/delayed.rb +++ b/lib/async/http/body/delayed.rb @@ -3,6 +3,7 @@ # Released under the MIT License. # Copyright, 2018-2023, by Samuel Williams. # Copyright, 2020, by Bruno Sutic. +# Copyright, 2023, by Thomas Morgan. require 'protocol/http/body/wrapper' diff --git a/license.md b/license.md index c9fd2273..22d2d13e 100644 --- a/license.md +++ b/license.md @@ -17,6 +17,7 @@ Copyright, 2021-2022, by Adam Daniels. Copyright, 2022, by Ian Ker-Seymer. Copyright, 2022, by Marco Concetto Rudilosso. Copyright, 2022, by Tim Meusel. +Copyright, 2023, by Thomas Morgan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/readme.md b/readme.md index 75bb6558..6eb705a9 100644 --- a/readme.md +++ b/readme.md @@ -367,6 +367,14 @@ We welcome contributions to this project. 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. +### Developer Certificate of Origin + +This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted. + +### Contributor Covenant + +This project is governed by [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms. + ## See Also - [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency. diff --git a/spec/async/http/body/pipe_spec.rb b/spec/async/http/body/pipe_spec.rb deleted file mode 100644 index 9f8ce2c8..00000000 --- a/spec/async/http/body/pipe_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020, by Bruno Sutic. -# Copyright, 2020-2023, by Samuel Williams. - -require 'async' -require 'async/http/body/pipe' -require 'async/http/body/writable' - -RSpec.describe Async::HTTP::Body::Pipe do - let(:input) { Async::HTTP::Body::Writable.new } - let(:pipe) { described_class.new(input) } - - let(:data) { 'Hello World!' } - - describe '#to_io' do - include_context Async::RSpec::Reactor - - let(:io) { pipe.to_io } - - before do - Async::Task.current.async do |task| # input writer task - first, second = data.split(' ') - input.write("#{first} ") - task.sleep(input_write_duration) if input_write_duration > 0 - input.write(second) - input.close - end - end - - after { io.close } - - shared_examples :returns_io_socket do - it 'returns an io socket' do - expect(io).to be_a(Async::IO::Socket) - expect(io.read).to eq data - end - end - - context 'when reading blocks' do - let(:input_write_duration) { 0.01 } - - include_examples :returns_io_socket - end - - context 'when reading does not block' do - let(:input_write_duration) { 0 } - - include_examples :returns_io_socket - end - end - - describe 'going out of reactor scope' do - context 'when pipe is closed' do - it 'finishes' do - Async { pipe.close } - end - end - - context 'when pipe is not closed' do - it 'finishes' do # ensures pipe background tasks are transient - Async { pipe } - end - end - end -end diff --git a/spec/async/http/body/slowloris_spec.rb b/spec/async/http/body/slowloris_spec.rb deleted file mode 100644 index 8ae0a1cf..00000000 --- a/spec/async/http/body/slowloris_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. - -require_relative 'writable_examples' - -require 'async/http/body/slowloris' - -RSpec.describe Async::HTTP::Body::Slowloris do - include_context Async::RSpec::Reactor - - it_behaves_like Async::HTTP::Body::Writable - - it "closes body with error if throughput is not maintained" do - subject.write("Hello World") - - sleep 0.1 - - expect do - subject.write("Hello World") - end.to raise_error(Async::HTTP::Body::Slowloris::ThroughputError, /Slow write/) - end - - it "doesn't close body if throughput is exceeded" do - subject.write("Hello World") - - expect do - subject.write("Hello World") - end.to_not raise_error - end -end diff --git a/spec/async/http/body/writable_examples.rb b/spec/async/http/body/writable_examples.rb deleted file mode 100644 index dca3a926..00000000 --- a/spec/async/http/body/writable_examples.rb +++ /dev/null @@ -1,114 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' -require 'async/reactor' - -require 'async/http/body' -require 'protocol/http/body/deflate' -require 'async/http/body/writable' -require 'async/http/endpoint' - -require 'async/io/ssl_socket' -require 'async/rspec/ssl' - -RSpec.shared_examples_for Async::HTTP::Body::Writable do - it "can write and read data" do - 3.times do |i| - subject.write("Hello World #{i}") - expect(subject.read).to be == "Hello World #{i}" - end - end - - it "can buffer data in order" do - 3.times do |i| - subject.write("Hello World #{i}") - end - - 3.times do |i| - expect(subject.read).to be == "Hello World #{i}" - end - end - - context '#join' do - it "can join chunks" do - 3.times do |i| - subject.write("#{i}") - end - - subject.close - - expect(subject.join).to be == "012" - end - end - - context '#each' do - it "can read all data in order" do - 3.times do |i| - subject.write("Hello World #{i}") - end - - subject.close - - 3.times do |i| - chunk = subject.read - expect(chunk).to be == "Hello World #{i}" - end - end - - it "can propagate failures" do - reactor.async do - expect do - subject.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_error(RuntimeError, /big/) - end - - expect{ - subject.write("Beep boop") # This will cause a failure. - Async::Task.current.yield - subject.write("Beep boop") # This will fail. - }.to raise_error(RuntimeError, /big/) - end - - it "can propagate failures in nested bodies" do - nested = Protocol::HTTP::Body::Deflate.for(subject) - - reactor.async do - expect do - nested.each do |chunk| - raise RuntimeError.new("It was too big!") - end - end.to raise_error(RuntimeError, /big/) - end - - expect{ - subject.write("Beep boop") # This will cause a failure. - Async::Task.current.yield - subject.write("Beep boop") # This will fail. - }.to raise_error(RuntimeError, /big/) - end - - it "will stop after finishing" do - output_task = reactor.async do - subject.each do |chunk| - expect(chunk).to be == "Hello World!" - end - end - - subject.write("Hello World!") - subject.close - - expect(subject).to_not be_empty - - Async::Task.current.yield - - expect(output_task).to be_finished - expect(subject).to be_empty - end - end -end diff --git a/spec/async/http/body/writable_spec.rb b/spec/async/http/body/writable_spec.rb deleted file mode 100644 index 8340ecf1..00000000 --- a/spec/async/http/body/writable_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. - -require_relative 'writable_examples' - -RSpec.describe Async::HTTP::Body::Writable do - include_context Async::RSpec::Reactor - - it_behaves_like Async::HTTP::Body::Writable -end diff --git a/spec/async/http/client_spec.rb b/spec/async/http/client_spec.rb deleted file mode 100644 index 19bc7b35..00000000 --- a/spec/async/http/client_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. - -require_relative 'server_context' - -require 'async/http/server' -require 'async/http/client' -require 'async/reactor' - -require 'async/io/ssl_socket' -require 'async/http/endpoint' -require 'protocol/http/accept_encoding' - -RSpec.describe Async::HTTP::Client, timeout: 5 do - describe Async::HTTP::Protocol::HTTP1 do - include_context Async::HTTP::Server - let(:protocol) {described_class} - - it "client can get resource" do - response = client.get("/") - response.read - expect(response).to be_success - end - end - - context 'non-existant host' do - include_context Async::RSpec::Reactor - - let(:endpoint) {Async::HTTP::Endpoint.parse('http://the.future')} - let(:client) {Async::HTTP::Client.new(endpoint)} - - it "should fail to connect" do - expect do - client.get("/") - end.to raise_error(SocketError, /not known/) - end - end - - describe Async::HTTP::Protocol::HTTPS do - include_context Async::RSpec::Reactor - - let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.codeotaku.com')} - let(:client) {Async::HTTP::Client.new(endpoint)} - - it "should specify hostname" do - expect(endpoint.hostname).to be == "www.codeotaku.com" - expect(client.authority).to be == "www.codeotaku.com" - end - - it "can request remote resource" do - 2.times do - response = client.get("/index") - expect(response).to be_success - response.finish - end - - client.close - end - - it "can request remote resource with compression" do - compressor = Protocol::HTTP::AcceptEncoding.new(client) - - response = compressor.get("/index", {'accept-encoding' => 'gzip'}) - - expect(response).to be_success - - expect(response.body).to be_kind_of Async::HTTP::Body::Inflate - expect(response.read).to be_start_with('') - - client.close - end - end -end diff --git a/spec/async/http/endpoint_spec.rb b/spec/async/http/endpoint_spec.rb deleted file mode 100644 index b02a6ec8..00000000 --- a/spec/async/http/endpoint_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. -# Copyright, 2021-2022, by Adam Daniels. - -require 'async/http/endpoint' - -RSpec.describe Async::HTTP::Endpoint do - it "should fail to parse relative url" do - expect{ - described_class.parse("/foo/bar") - }.to raise_error(ArgumentError, /absolute/) - end - - describe '#port' do - let(:url_string) {"https://localhost:9292"} - - it "extracts port from URL" do - endpoint = Async::HTTP::Endpoint.parse(url_string) - - expect(endpoint.port).to eq 9292 - end - - it "extracts port from options" do - endpoint = Async::HTTP::Endpoint.parse(url_string, port: 9000) - - expect(endpoint.port).to eq 9000 - end - end - - describe '#hostname' do - describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292") do - it {is_expected.to have_attributes(hostname: '127.0.0.1')} - - it "should be connecting to 127.0.0.1" do - expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: '127.0.0.1') - end - end - - describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292", hostname: 'localhost') do - it {is_expected.to have_attributes(hostname: 'localhost')} - it {is_expected.to_not be_localhost} - - it "should be connecting to localhost" do - expect(subject.endpoint).to be_a Async::IO::SSLEndpoint - expect(subject.endpoint).to have_attributes(hostname: '127.0.0.1') - expect(subject.endpoint.endpoint).to have_attributes(hostname: 'localhost') - end - end - end - - describe '.for' do - context Async::HTTP::Endpoint.for("http", "localhost") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/")} - it {is_expected.to_not be_secure} - end - - context Async::HTTP::Endpoint.for("http", "localhost", "/foo") do - it {is_expected.to have_attributes(scheme: "http", hostname: "localhost", path: "/foo")} - end - end - - describe '#secure?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to_not be_secure } - end - - context 'https://localhost' do - it { is_expected.to be_secure } - end - - context 'with scheme: https' do - subject {Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https')} - - it { is_expected.to be_secure } - end - end - - describe '#localhost?' do - subject {Async::HTTP::Endpoint.parse(description)} - - context 'http://localhost' do - it { is_expected.to be_localhost } - end - - context 'http://hello.localhost' do - it { is_expected.to be_localhost } - end - - context 'http://localhost.' do - it { is_expected.to be_localhost } - end - - context 'http://hello.localhost.' do - it { is_expected.to be_localhost } - end - - context 'http://localhost.com' do - it { is_expected.to_not be_localhost } - end - end - - describe '#path' do - it "can normal urls" do - endpoint = Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" - end - - it "can handle websocket urls" do - endpoint = Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") - expect(endpoint.path).to be == "/bar?baz" - end - end -end - -RSpec.describe "http://www.google.com/search" do - let(:endpoint) {Async::HTTP::Endpoint.parse(subject)} - - it "should be valid endpoint" do - expect{endpoint}.to_not raise_error - end - - it "should select the correct protocol" do - expect(endpoint.protocol).to be Async::HTTP::Protocol::HTTP1 - end - - it "should parse the correct hostname" do - expect(endpoint.hostname).to be == "www.google.com" - end - - it "should not be equal if path is different" do - other = Async::HTTP::Endpoint.parse('http://www.google.com/search?q=ruby') - expect(endpoint).to_not be_eql other - end -end diff --git a/spec/async/http/internet_spec.rb b/spec/async/http/internet_spec.rb deleted file mode 100644 index 15597b48..00000000 --- a/spec/async/http/internet_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. - -require 'async/http/internet' -require 'async/reactor' - -require 'json' - -RSpec.describe Async::HTTP::Internet, timeout: 30 do - include_context Async::RSpec::Reactor - - let(:headers) {[['accept', '*/*'], ['user-agent', 'async-http']]} - - after do - subject.close - end - - it "can fetch remote website" do - response = subject.get("https://www.codeotaku.com/index", headers) - - expect(response).to be_success - - response.close - end - - let(:sample) {{"hello" => "world"}} - let(:body) {[JSON.dump(sample)]} - - # This test is increasingly flakey. - xit "can fetch remote json" do - response = subject.post("https://httpbin.org/anything", headers, body) - - expect(response).to be_success - expect{JSON.parse(response.read)}.to_not raise_error - end -end diff --git a/spec/async/http/performance_spec.rb b/spec/async/http/performance_spec.rb deleted file mode 100755 index acb3df2b..00000000 --- a/spec/async/http/performance_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' - -require_relative 'server_context' -require 'async/container' - -require 'etc' - -RSpec.shared_examples_for 'client benchmark' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:9294', timeout: 0.8, reuse_port: true)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, []] - end - end - - let(:url) {endpoint.url.to_s} - let(:repeats) {1000} - let(:concurrency) {Etc.nprocessors || 2} - - before do - Sync do - # We bind the endpoint before running the server so that we know incoming connections will be accepted: - @bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint) - end - - # I feel a dedicated class might be better than this hack: - allow(@bound_endpoint).to receive(:protocol).and_return(endpoint.protocol) - allow(@bound_endpoint).to receive(:scheme).and_return(endpoint.scheme) - - @container = Async::Container.new - - GC.disable - - @container.run(count: concurrency) do |instance| - Async do - instance.ready! - server.run - end - end - - @bound_endpoint.close - end - - after do - @container.stop - - GC.enable - end - - it "runs benchmark", timeout: nil do - if ab = `which ab`.chomp! - system(ab, "-k", "-n", (concurrency*repeats).to_s, "-c", concurrency.to_s, url) - end - - if wrk = `which wrk`.chomp! - system(wrk, "-c", concurrency.to_s, "-d", "2", "-t", concurrency.to_s, url) - end - end -end - -RSpec.describe Async::HTTP::Server do - describe Protocol::HTTP::Middleware::Okay do - let(:server) do - Async::HTTP::Server.new( - Protocol::HTTP::Middleware::Okay, - @bound_endpoint - ) - end - - include_examples 'client benchmark' - end - - describe 'multiple chunks' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do - Protocol::HTTP::Response[200, {}, "Hello World".chars] - end - end - - include_examples 'client benchmark' - end -end diff --git a/spec/async/http/protocol/http11_spec.rb b/spec/async/http/protocol/http11_spec.rb deleted file mode 100755 index d16bf8ba..00000000 --- a/spec/async/http/protocol/http11_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. -# Copyright, 2018, by Janko Marohnić. -# Copyright, 2023, by Thomas Morgan. - -require 'async/http/protocol/http11' -require_relative 'shared_examples' - -RSpec.describe Async::HTTP::Protocol::HTTP11 do - it_behaves_like Async::HTTP::Protocol - - context 'bad requests' do - include_context Async::HTTP::Server - - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "should fail cleanly when path is empty" do - response = client.get("") - - expect(response.status).to be == 400 - end - end - - context 'head request' do - include_context Async::HTTP::Server - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, ["Hello", "World"]] - end - end - - it "doesn't reply with body" do - 5.times do - response = client.head("/") - - expect(response).to be_success - expect(response.version).to be == "HTTP/1.1" - expect(response.body).to be_empty - - response.read - end - end - end - - context 'raw response' do - include_context Async::HTTP::Server - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - peer = request.hijack! - - peer.write( - "#{request.version} 200 It worked!\r\n" + - "connection: close\r\n" + - "\r\n" + - "Hello World!" - ) - peer.close - - nil - end - end - - it "reads raw response" do - response = client.get("/") - - expect(response.read).to be == "Hello World!" - end - end -end diff --git a/spec/async/http/protocol/http2_spec.rb b/spec/async/http/protocol/http2_spec.rb deleted file mode 100644 index 2e8a52a8..00000000 --- a/spec/async/http/protocol/http2_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. - -require 'async/http/protocol/http2' -require_relative 'shared_examples' - -RSpec.describe Async::HTTP::Protocol::HTTP2 do - it_behaves_like Async::HTTP::Protocol - - context 'bad requests' do - include_context Async::HTTP::Server - - it "should fail with explicit authority" do - expect do - client.post("/", [[':authority', 'foo']]) - end.to raise_error(Protocol::HTTP2::StreamError) - end - end - - context 'closed streams' do - include_context Async::HTTP::Server - - it 'should delete stream after response stream is closed' do - response = client.get("/") - connection = response.connection - - response.read - - expect(connection.streams).to be_empty - end - end - - context 'host header' do - include_context Async::HTTP::Server - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] - end - end - - # We specify nil for the authority - it won't be sent. - let!(:client) {Async::HTTP::Client.new(endpoint, authority: nil)} - - it "should not send :authority header if host header is present" do - response = client.post("/", [['host', 'foo']]) - - expect(response.headers).to include('host') - expect(response.headers['host']).to be == 'foo' - - # TODO Should HTTP/2 respect host header? - expect(response.read).to be == "Authority: nil" - end - end - - context 'stopping requests' do - include_context Async::HTTP::Server - - let(:notification) {Async::Notification.new} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new - - reactor.async do |task| - begin - 100.times do |i| - body.write("Chunk #{i}") - task.sleep (0.01) - end - rescue - # puts "Response generation failed: #{$!}" - ensure - body.close - notification.signal - end - end - - Protocol::HTTP::Response[200, {}, body] - end - end - - let(:pool) {client.pool} - - it "should close stream without closing connection" do - expect(pool).to be_empty - - response = client.get("/") - - expect(pool).to_not be_empty - - response.close - - notification.wait - - expect(response.stream.connection).to be_reusable - end - end -end diff --git a/spec/async/http/protocol/shared_examples.rb b/spec/async/http/protocol/shared_examples.rb deleted file mode 100644 index b29873d5..00000000 --- a/spec/async/http/protocol/shared_examples.rb +++ /dev/null @@ -1,553 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. -# Copyright, 2020, by Igor Sidorov. - -require_relative '../server_context' - -require 'async' -require 'async/clock' -require 'async/http/client' -require 'async/http/server' -require 'async/http/endpoint' -require 'async/http/body/hijack' -require 'tempfile' - -require 'protocol/http/body/file' - -require 'async/rspec/profile' - -RSpec.shared_examples_for Async::HTTP::Protocol do - include_context Async::HTTP::Server - - it "should have valid scheme" do - expect(client.scheme).to be == "http" - end - - context '#close' do - it 'can close the connection' do - Async do |task| - response = client.get("/") - expect(response).to be_success - response.finish - - client.close - - expect(task.children).to be_empty - end.wait - end - end - - context "huge body", timeout: 600 do - let(:body) {Protocol::HTTP::Body::File.open("/dev/zero", size: 512*1024**2)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, body] - end - end - - it "client can download data quickly" do |example| - response = client.get("/") - expect(response).to be_success - - data_size = 0 - duration = Async::Clock.measure do - while chunk = response.body.read - data_size += chunk.bytesize - chunk.clear - end - - response.finish - end - - size_mbytes = data_size / 1024**2 - - example.reporter.message "Data size: #{size_mbytes}MB Duration: #{duration.round(2)}s Throughput: #{(size_mbytes / duration).round(2)}MB/s" - end - end - - context 'buffered body' do - let(:body) {Async::HTTP::Body::Buffered.new(["Hello World"])} - let(:response) {Protocol::HTTP::Response[200, {}, body]} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - response - end - end - - it "response body should be closed" do - expect(body).to receive(:close).and_call_original - # expect(response).to receive(:close).and_call_original - - expect(client.get("/", {}).read).to be == "Hello World" - end - end - - context 'empty body' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[204] - end - end - - it 'properly handles no content responses' do - expect(client.get("/", {}).read).to be_nil - end - end - - context 'with trailer', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if trailer = request.headers['trailer'] - expect(request.headers).to_not include('etag') - request.finish - expect(request.headers).to include('etag') - - Protocol::HTTP::Response[200, [], "request trailer"] - else - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("response trailer") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - Protocol::HTTP::Response[200, headers, body] - end - end - end - - it "can send request trailer" do - headers = Protocol::HTTP::Headers.new - headers.add('trailer', 'etag') - body = Async::HTTP::Body::Writable.new - - Async do |task| - body.write("Hello") - task.sleep(0.01) - headers.add('etag', 'abcd') - body.close - end - - response = client.post("/", headers, body) - expect(response.read).to be == "request trailer" - - expect(response).to be_success - end - - it "can receive response trailer" do - response = client.get("/") - expect(response.headers).to include('trailer') - headers = response.headers - expect(headers).to_not include('etag') - - expect(response.read).to be == "response trailer" - expect(response).to be_success - - # It was sent as a trailer. - expect(headers).to include('etag') - end - end - - context 'with working server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.method == 'POST' - # We stream the request body directly to the response. - Protocol::HTTP::Response[200, {}, request.body] - elsif request.method == 'GET' - expect(request.body).to be nil - - Protocol::HTTP::Response[200, { - 'remote-address' => request.remote_address.inspect - }, ["#{request.method} #{request.version}"]] - else - Protocol::HTTP::Response[200, {}, ["Hello World"]] - end - end - end - - it "should have valid scheme" do - expect(server.scheme).to be == "http" - end - - it "disconnects slow clients" do - response = client.get("/") - response.read - - # We expect this connection to be closed: - connection = response.connection - - reactor.sleep(1.0) - - response = client.get("/") - response.read - - expect(connection).to_not be_reusable - - # client.close - # reactor.sleep(0.1) - # reactor.print_hierarchy - end - - context 'using GET method' do - let(:expected) {"GET #{protocol::VERSION}"} - - it "can handle many simultaneous requests", timeout: 10 do |example| - duration = Async::Clock.measure do - 10.times do - tasks = 100.times.collect do - Async do - client.get("/") - end - end - - tasks.each do |task| - response = task.wait - expect(response).to be_success - expect(response.read).to eq expected - end - end - end - - example.reporter.message "Pool: #{client.pool}" - example.reporter.message "Duration = #{duration.round(2)}" - end - - context 'with response' do - let(:response) {client.get("/")} - after {response.finish} - - it "can finish gracefully" do - expect(response).to be_success - end - - it "is successful" do - expect(response).to be_success - expect(response.read).to eq expected - end - - it "provides content length" do - expect(response.body.length).to_not be_nil - end - - let(:tempfile) {Tempfile.new} - - it "can save to disk" do - response.save(tempfile.path) - expect(tempfile.read).to eq expected - - tempfile.close - end - - it "has remote-address header" do - expect(response.headers['remote-address']).to_not be_nil - end - - it "has protocol version" do - expect(response.version).to_not be_nil - end - end - end - - context 'HEAD' do - let(:response) {client.head("/")} - after {response.finish} - - it "is successful and without body" do - expect(response).to be_success - expect(response.body).to_not be_nil - expect(response.body).to be_empty - expect(response.body.length).to_not be_nil - expect(response.read).to be_nil - end - end - - context 'POST' do - let(:response) {client.post("/", {}, ["Hello", " ", "World"])} - - after {response.finish} - - it "is successful" do - expect(response).to be_success - expect(response.read).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "can buffer response" do - buffer = response.finish - - expect(buffer.join).to be == "Hello World" - - expect(client.pool).to_not be_busy - end - - it "should not contain content-length response header" do - expect(response.headers).to_not include('content-length') - end - - it "fails gracefully when closing connection" do - client.pool.acquire do |connection| - connection.stream.close - end - end - end - end - - context 'content length' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, [], ["Content Length: #{request.body.length}"]] - end - end - - it "can send push promises" do - response = client.post("/test", [], ["Hello World!"]) - expect(response).to be_success - - expect(response.body.length).to be == 18 - expect(response.read).to be == "Content Length: 12" - end - end - - context 'hijack with nil response' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - nil - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect(response).to be_server_failure - end - end - - context 'partial hijack' do - let(:content) {"Hello World!"} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| - stream.write content - stream.write content - stream.close - end - end - end - - it "reads hijacked body" do - response = client.get("/") - - expect(response.read).to be == (content*2) - end - end - - context 'body with incorrect length' do - let(:bad_body) {Async::HTTP::Body::Buffered.new(["Borked"], 10)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, bad_body] - end - end - - it "fails with appropriate error" do - response = client.get("/") - - expect do - response.read - end.to raise_error(EOFError) - end - end - - context 'streaming server' do - let!(:sent_chunks) {[]} - - let(:server) do - chunks = sent_chunks - - Async::HTTP::Server.for(@bound_endpoint) do |request| - body = Async::HTTP::Body::Writable.new - - Async::Reactor.run do |task| - 10.times do |i| - chunk = "Chunk #{i}" - chunks << chunk - - body.write chunk - task.sleep 0.25 - end - - body.finish - end - - Protocol::HTTP::Response[200, {}, body] - end - end - - it "can cancel response" do - response = client.get("/") - - expect(response.body.read).to be == "Chunk 0" - - response.close - - expect(sent_chunks).to be == ["Chunk 0"] - end - end - - context 'hijack server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - if request.hijack? - io = request.hijack! - io.write "HTTP/1.1 200 Okay\r\nContent-Length: 16\r\n\r\nHijack Succeeded" - io.flush - io.close - else - Protocol::HTTP::Response[200, {}, ["Hijack Failed"]] - end - end - end - - it "will hijack response if possible" do - response = client.get("/") - - expect(response.read).to include("Hijack") - end - end - - context 'broken server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - raise RuntimeError.new('simulated failure') - end - end - - it "can't get /" do - expect do - response = client.get("/") - end.to raise_error(Exception) - end - end - - context 'slow server' do - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:0', reuse_port: true, timeout: 0.1)} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::Task.current.sleep(endpoint.timeout * 2) - Protocol::HTTP::Response[200, {}, []] - end - end - - it "can't get /" do - expect do - client.get("/") - end.to raise_error(Async::TimeoutError) - end - end - - context 'bi-directional streaming', if: described_class.bidirectional? do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - # Echo the request body back to the client. - Protocol::HTTP::Response[200, {}, request.body] - end - end - - it "can read from request body and write response body simultaneously" do - body = Async::HTTP::Body::Writable.new - - # Ideally, the flow here is as follows: - # 1/ Client writes headers to server. - # 2/ Client starts writing data to server (in async task). - # 3/ Client reads headers from server. - # 4a/ Client reads data from server. - # 4b/ Client finishes sending data to server. - response = client.post(endpoint.path, [], body) - - expect(response).to be_success - - body.write "." - count = 0 - - response.each do |chunk| - if chunk.bytesize > 32 - body.close - else - count += 1 - body.write chunk*2 - Async::Task.current.sleep(0.1) - end - end - - expect(count).to be == 6 - end - end - - context 'multiple client requests' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, [request.path]] - end - end - - around do |example| - current = Console.logger.level - Console.logger.fatal! - - example.run - ensure - Console.logger.level = current - end - - it "doesn't cancel all requests" do - tasks = [] - task = Async::Task.current - stopped = [] - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/a').finish - end - ensure - stopped << 'a' - end - } - end - - 10.times do - tasks << task.async { - begin - loop do - client.get('http://127.0.0.1:8080/b').finish - end - ensure - stopped << 'b' - end - } - end - - tasks.each do |child| - task.sleep 0.01 - child.stop - end - - expect(stopped.sort).to be == stopped - end - end -end diff --git a/spec/async/http/retry_spec.rb b/spec/async/http/retry_spec.rb deleted file mode 100644 index 2c648ad4..00000000 --- a/spec/async/http/retry_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2020-2023, by Samuel Williams. - -require_relative 'server_context' - -require 'async/http/client' -require 'async/http/endpoint' - -RSpec.describe 'consistent retry behaviour' do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} - - let(:delay) {0.1} - let(:retries) {2} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Async::Task.current.sleep(delay) - Protocol::HTTP::Response[200, {}, []] - end - end - - def make_request(body) - # This causes the first request to fail with "SocketError" which is retried: - Async::Task.current.with_timeout(delay / 2, SocketError) do - return client.get('/', {}, body) - end - end - - specify 'with nil body' do - make_request(nil) - end - - specify 'with empty array body' do - make_request([]) - end -end diff --git a/spec/async/http/server_context.rb b/spec/async/http/server_context.rb deleted file mode 100644 index 82779170..00000000 --- a/spec/async/http/server_context.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' -require 'async/http/endpoint' -require 'async/io/shared_endpoint' - -RSpec.shared_context Async::HTTP::Server do - include_context Async::RSpec::Reactor - - let(:protocol) {described_class} - let(:endpoint) {Async::HTTP::Endpoint.parse('http://127.0.0.1:0', timeout: 0.8, reuse_port: true, protocol: protocol)} - - let(:server_endpoint) {endpoint} - let(:client_endpoint) {endpoint} - - let(:retries) {1} - - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - Protocol::HTTP::Response[200, {}, []] - end - end - - before do - # We bind the endpoint before running the server so that we know incoming connections will be accepted: - @bound_endpoint = Async::IO::SharedEndpoint.bound(server_endpoint) - - # I feel a dedicated class might be better than this hack: - allow(@bound_endpoint).to receive(:protocol).and_return(server_endpoint.protocol) - allow(@bound_endpoint).to receive(:scheme).and_return(server_endpoint.scheme) - - @server_task = Async do - server.run - end - - local_address_endpoint = @bound_endpoint.local_address_endpoint - - if timeout = client_endpoint.timeout - local_address_endpoint.each do |endpoint| - endpoint.options = {timeout: timeout} - end - end - - client_endpoint.endpoint = local_address_endpoint - @client = Async::HTTP::Client.new(client_endpoint, protocol: client_endpoint.protocol, retries: retries) - end - - after do - @client&.close - @server_task&.stop - @bound_endpoint&.close - end - - let(:client) {@client} -end diff --git a/spec/async/http/ssl_spec.rb b/spec/async/http/ssl_spec.rb deleted file mode 100644 index c2586a37..00000000 --- a/spec/async/http/ssl_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. - -require 'async/http/server' -require 'async/http/client' -require 'async/http/endpoint' - -require 'async/io/ssl_socket' - -require 'async/rspec/reactor' -require 'async/rspec/ssl' - -RSpec.describe Async::HTTP::Server, timeout: 5 do - include_context Async::RSpec::Reactor - include_context Async::RSpec::SSL::ValidCertificate - - describe "application layer protocol negotiation" do - let(:server_context) do - OpenSSL::SSL::SSLContext.new.tap do |context| - context.cert = certificate - - context.alpn_select_cb = lambda do |protocols| - protocols.last - end - - context.key = key - end - end - - let(:client_context) do - OpenSSL::SSL::SSLContext.new.tap do |context| - context.cert_store = certificate_store - - context.alpn_protocols = ["h2", "http/1.1"] - - context.verify_mode = OpenSSL::SSL::VERIFY_PEER - end - end - - # Shared port for localhost network tests. - let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: server_context)} - let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:6779", ssl_context: client_context)} - - it "client can get a resource via https" do - server = Async::HTTP::Server.for(server_endpoint, protocol: Async::HTTP::Protocol::HTTP1) do |request| - Protocol::HTTP::Response[200, {}, ['Hello World']] - end - - client = Async::HTTP::Client.new(client_endpoint) - - Async do |task| - server_task = task.async do - server.run - end - - response = client.get("/") - - expect(response).to be_success - expect(response.read).to be == "Hello World" - - client.close - server_task.stop - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 3dc0d204..00000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2017-2023, by Samuel Williams. -# Copyright, 2018, by Janko Marohnić. - -require 'traces' - -require 'bundler/setup' -require 'covered/rspec' - -require 'async/rspec' - -ENV['TRACES_BACKEND'] ||= 'traces/backend/test' - -RSpec.shared_context 'docstring as description' do - let(:description) {self.class.metadata.fetch(:description_args).first} -end - -RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = ".rspec_status" - - config.include_context 'docstring as description' - - config.expect_with :rspec do |c| - c.syntax = :expect - end -end diff --git a/spec/async/http/body_spec.rb b/test/async/http/body.rb similarity index 55% rename from spec/async/http/body_spec.rb rename to test/async/http/body.rb index e16e13f9..e08ad660 100644 --- a/spec/async/http/body_spec.rb +++ b/test/async/http/body.rb @@ -5,22 +5,15 @@ require 'async/http/body' -require 'async/http/server' -require 'async/http/client' -require 'async/http/endpoint' - -require 'async/io/ssl_socket' - -require_relative 'server_context' - +require 'sus/fixtures/async' +require 'sus/fixtures/openssl' +require 'sus/fixtures/async/http' require 'localhost/authority' -RSpec.shared_examples Async::HTTP::Body do - let(:client) {Async::HTTP::Client.new(client_endpoint, protocol: described_class)} - - context 'with echo server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint, protocol: described_class) do |request| +ABody = Sus::Shared("a body") do + with 'echo server' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| input = request.body output = Async::HTTP::Body::Writable.new @@ -46,16 +39,16 @@ response = client.post("/", {}, output) - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "!dlroW olleH" end end - context "with streaming server" do + with "streaming server" do let(:notification) {Async::Notification.new} - let(:server) do - Async::HTTP::Server.for(@bound_endpoint, protocol: described_class) do |request| + let(:app) do + Protocol::HTTP::Middleware.for do |request| body = Async::HTTP::Body::Writable.new Async::Task.current.async do |task| @@ -74,7 +67,7 @@ it "can stream response" do response = client.get("/") - expect(response).to be_success + expect(response).to be(:success?) j = 0 # This validates interleaving @@ -88,23 +81,28 @@ end end -RSpec.describe Async::HTTP::Protocol::HTTP1 do - include_context Async::HTTP::Server +describe Async::HTTP::Protocol::HTTP1 do + include Sus::Fixtures::Async::HTTP::ServerContext - it_should_behave_like Async::HTTP::Body + it_behaves_like ABody end -RSpec.describe Async::HTTP::Protocol::HTTPS do - include_context Async::HTTP::Server +describe Async::HTTP::Protocol::HTTPS do + include Sus::Fixtures::Async::HTTP::ServerContext + include Sus::Fixtures::OpenSSL::ValidCertificateContext let(:authority) {Localhost::Authority.new} let(:server_context) {authority.server_context} let(:client_context) {authority.client_context} - # Shared port for localhost network tests. - let(:server_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:0", ssl_context: server_context, reuse_port: true)} - let(:client_endpoint) {Async::HTTP::Endpoint.parse("https://localhost:0", ssl_context: client_context, reuse_port: true)} + def make_server_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: server_context) + end + + def make_client_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: client_context) + end - it_should_behave_like Async::HTTP::Body + it_behaves_like ABody end diff --git a/spec/async/http/body/hijack_spec.rb b/test/async/http/body/hijack.rb similarity index 56% rename from spec/async/http/body/hijack_spec.rb rename to test/async/http/body/hijack.rb index 5397180f..73cad02d 100644 --- a/spec/async/http/body/hijack_spec.rb +++ b/test/async/http/body/hijack.rb @@ -5,42 +5,44 @@ require 'async/http/body/hijack' -RSpec.describe Async::HTTP::Body::Hijack do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' + +describe Async::HTTP::Body::Hijack do + include Sus::Fixtures::Async::ReactorContext + + let(:body) do + subject.wrap do |stream| + 3.times do + stream.write(content) + end + stream.close + end + end let(:content) {"Hello World!"} - describe '#call' do + with '#call' do let(:stream) {Async::HTTP::Body::Writable.new} - subject do - described_class.wrap do |stream| - 3.times do - stream.write(content) - end - stream.close - end - end - it "should generate body using direct invocation" do - subject.call(stream) + body.call(stream) 3.times do expect(stream.read).to be == content end expect(stream.read).to be_nil - expect(stream).to be_empty + expect(stream).to be(:empty?) end it "should generate body using stream" do 3.times do - expect(subject.read).to be == content + expect(body.read).to be == content end - expect(subject.read).to be_nil + expect(body.read).to be_nil - expect(subject).to be_empty + expect(body).to be(:empty?) end end end diff --git a/test/async/http/body/pipe.rb b/test/async/http/body/pipe.rb new file mode 100644 index 00000000..fe620ec9 --- /dev/null +++ b/test/async/http/body/pipe.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020, by Bruno Sutic. +# Copyright, 2020-2023, by Samuel Williams. + +require 'async' +require 'async/http/body/pipe' +require 'async/http/body/writable' + +require 'sus/fixtures/async' + +describe Async::HTTP::Body::Pipe do + let(:input) {Async::HTTP::Body::Writable.new} + let(:pipe) {subject.new(input)} + + let(:data) {'Hello World!'} + + with '#to_io' do + include Sus::Fixtures::Async::ReactorContext + + let(:input_write_duration) {0} + let(:io) { pipe.to_io } + + def before + super + + # input writer task + Async do |task| + first, second = data.split(' ') + input.write("#{first} ") + task.sleep(input_write_duration) if input_write_duration > 0 + input.write(second) + input.close + end + end + + def aftrer + io.close + + super + end + + it "returns an io socket" do + expect(io).to be_a(Async::IO::Socket) + expect(io.read).to be == data + end + + with 'blocking reads' do + let(:input_write_duration) {0.01} + + it 'returns an io socket' do + expect(io.read).to be == data + end + end + end + + with 'reactor going out of scope' do + it 'finishes' do + # ensures pipe background tasks are transient + Async{pipe} + end + + with 'closed pipe' do + it 'finishes' do + Async{pipe.close} + end + end + end +end diff --git a/test/async/http/body/slowloris.rb b/test/async/http/body/slowloris.rb new file mode 100644 index 00000000..dc3e48be --- /dev/null +++ b/test/async/http/body/slowloris.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2019-2023, by Samuel Williams. + +require 'async/http/body/slowloris' + +require 'sus/fixtures/async' +require 'async/http/body/a_writable_body' + +describe Async::HTTP::Body::Slowloris do + include Sus::Fixtures::Async::ReactorContext + + let(:body) {subject.new} + + it_behaves_like Async::HTTP::Body::AWritableBody + + it "closes body with error if throughput is not maintained" do + body.write("Hello World") + + sleep 0.1 + + expect do + body.write("Hello World") + end.to raise_exception(Async::HTTP::Body::Slowloris::ThroughputError, message: be =~ /Slow write/) + end + + it "doesn't close body if throughput is exceeded" do + body.write("Hello World") + + expect do + body.write("Hello World") + end.not.to raise_exception + end +end diff --git a/test/async/http/body/writable.rb b/test/async/http/body/writable.rb new file mode 100644 index 00000000..9d553a58 --- /dev/null +++ b/test/async/http/body/writable.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. + +require 'async/http/body/slowloris' + +require 'sus/fixtures/async' +require 'async/http/body/a_writable_body' + +describe Async::HTTP::Body::Writable do + include Sus::Fixtures::Async::ReactorContext + + let(:body) {subject.new} + + it_behaves_like Async::HTTP::Body::AWritableBody +end diff --git a/test/async/http/client.rb b/test/async/http/client.rb new file mode 100644 index 00000000..3a4cdb91 --- /dev/null +++ b/test/async/http/client.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2017-2023, by Samuel Williams. + +require 'async/http/server' +require 'async/http/client' +require 'async/reactor' + +require 'async/io/ssl_socket' +require 'async/http/endpoint' +require 'protocol/http/accept_encoding' + +require 'sus/fixtures/async' +require 'sus/fixtures/async/http' + +describe Async::HTTP::Client do + with 'basic server' do + include Sus::Fixtures::Async::HTTP::ServerContext + + it "client can get resource" do + response = client.get("/") + response.read + expect(response).to be(:success?) + end + end + + with 'non-existant host' do + include Sus::Fixtures::Async::ReactorContext + + let(:endpoint) {Async::HTTP::Endpoint.parse('http://the.future')} + let(:client) {Async::HTTP::Client.new(endpoint)} + + it "should fail to connect" do + expect do + client.get("/") + end.to raise_exception(SocketError, message: be =~ /not known/) + end + end +end diff --git a/test/async/http/client/codeotaku.rb b/test/async/http/client/codeotaku.rb new file mode 100644 index 00000000..5dfe7487 --- /dev/null +++ b/test/async/http/client/codeotaku.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'async/http/client' +require 'async/http/endpoint' +require 'protocol/http/accept_encoding' + +require 'sus/fixtures/async' + +describe Async::HTTP::Client do + include Sus::Fixtures::Async::ReactorContext + + let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.codeotaku.com')} + let(:client) {Async::HTTP::Client.new(endpoint)} + + it "should specify hostname" do + expect(endpoint.hostname).to be == "www.codeotaku.com" + expect(client.authority).to be == "www.codeotaku.com" + end + + it 'can fetch remote resource' do + response = client.get('/index') + + response.finish + + expect(response).not.to be(:failure?) + end + + it "can request remote resource with compression" do + compressor = Protocol::HTTP::AcceptEncoding.new(client) + + response = compressor.get("/index", {'accept-encoding' => 'gzip'}) + + expect(response).to be(:success?) + + expect(response.body).to be_a Async::HTTP::Body::Inflate + expect(response.read).to be(:start_with?, '') + end +end + diff --git a/spec/async/http/client/google_spec.rb b/test/async/http/client/google.rb similarity index 73% rename from spec/async/http/client/google_spec.rb rename to test/async/http/client/google.rb index 63f583c4..f8c8ae27 100644 --- a/spec/async/http/client/google_spec.rb +++ b/test/async/http/client/google.rb @@ -6,18 +6,20 @@ require 'async/http/client' require 'async/http/endpoint' -RSpec.describe Async::HTTP::Client, timeout: 5 do - include_context Async::RSpec::Reactor +require 'sus/fixtures/async' + +describe Async::HTTP::Client do + include Sus::Fixtures::Async::ReactorContext let(:endpoint) {Async::HTTP::Endpoint.parse('https://www.google.com')} let(:client) {Async::HTTP::Client.new(endpoint)} it 'can fetch remote resource' do response = client.get('/', 'accept' => '*/*') - + response.finish - - expect(response).to_not be_failure + + expect(response).not.to be(:failure?) client.close end diff --git a/test/async/http/endpoint.rb b/test/async/http/endpoint.rb new file mode 100644 index 00000000..d14b1464 --- /dev/null +++ b/test/async/http/endpoint.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. +# Copyright, 2021-2022, by Adam Daniels. + +require 'async/http/endpoint' + +describe Async::HTTP::Endpoint do + it "should fail to parse relative url" do + expect do + subject.parse("/foo/bar") + end.to raise_exception(ArgumentError, message: be =~ /absolute/) + end + + with '#port' do + let(:url_string) {"https://localhost:9292"} + + it "extracts port from URL" do + endpoint = Async::HTTP::Endpoint.parse(url_string) + + expect(endpoint).to have_attributes(port: be == 9292) + end + + it "extracts port from options" do + endpoint = Async::HTTP::Endpoint.parse(url_string, port: 9000) + + expect(endpoint).to have_attributes(port: be == 9000) + end + end + + with '#hostname' do + describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292") do + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == '127.0.0.1') + end + + it "should be connecting to 127.0.0.1" do + expect(subject.endpoint).to be_a Async::IO::SSLEndpoint + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == '127.0.0.1') + end + end + + describe Async::HTTP::Endpoint.parse("https://127.0.0.1:9292", hostname: 'localhost') do + it 'has correct hostname' do + expect(subject).to have_attributes(hostname: be == 'localhost') + expect(subject).not.to be(:localhost?) + end + + it "should be connecting to localhost" do + expect(subject.endpoint).to be_a Async::IO::SSLEndpoint + expect(subject.endpoint).to have_attributes(hostname: be == '127.0.0.1') + expect(subject.endpoint.endpoint).to have_attributes(hostname: be == 'localhost') + end + end + end + + with '.for' do + describe Async::HTTP::Endpoint.for("http", "localhost") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/" + ) + + expect(subject).not.to be(:secure?) + end + end + + describe Async::HTTP::Endpoint.for("http", "localhost", "/foo") do + it "should have correct attributes" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "localhost", + path: be == "/foo" + ) + + expect(subject).not.to be(:secure?) + end + end + end + + with '#secure?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should not be secure" do + expect(subject).not.to be(:secure?) + end + end + + describe Async::HTTP::Endpoint.parse("https://localhost") do + it "should be secure" do + expect(subject).to be(:secure?) + end + end + + with 'scheme: https' do + describe Async::HTTP::Endpoint.parse("http://localhost", scheme: 'https') do + it "should be secure" do + expect(subject).to be(:secure?) + end + end + end + end + + with '#localhost?' do + describe Async::HTTP::Endpoint.parse("http://localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end + end + + describe Async::HTTP::Endpoint.parse("http://hello.localhost") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end + end + + describe Async::HTTP::Endpoint.parse("http://localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end + end + + describe Async::HTTP::Endpoint.parse("http://hello.localhost.") do + it "should be localhost" do + expect(subject).to be(:localhost?) + end + end + + describe Async::HTTP::Endpoint.parse("http://localhost.com") do + it "should not be localhost" do + expect(subject).not.to be(:localhost?) + end + end + end + + with '#path' do + describe Async::HTTP::Endpoint.parse("http://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end + end + + with 'websocket scheme' do + describe Async::HTTP::Endpoint.parse("wss://foo.com/bar?baz") do + it "should have correct path" do + expect(subject).to have_attributes(path: be == "/bar?baz") + end + end + end + end +end + +describe Async::HTTP::Endpoint.parse("http://www.google.com/search") do + it "should select the correct protocol" do + expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP1 + end + + it "should parse the correct hostname" do + expect(subject).to have_attributes( + scheme: be == "http", + hostname: be == "www.google.com", + path: be == "/search" + ) + end + + it "should not be equal if path is different" do + other = Async::HTTP::Endpoint.parse('http://www.google.com/search?q=ruby') + expect(subject).not.to be == other + expect(subject).not.to be(:eql?, other) + end +end diff --git a/test/async/http/internet.rb b/test/async/http/internet.rb new file mode 100644 index 00000000..ac25f408 --- /dev/null +++ b/test/async/http/internet.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. + +require 'async/http/internet' +require 'async/reactor' + +require 'json' +require 'sus/fixtures/async' + +describe Async::HTTP::Internet do + include Sus::Fixtures::Async::ReactorContext + + let(:internet) {subject.new} + let(:headers) {[['accept', '*/*'], ['user-agent', 'async-http']]} + + it "can fetch remote website" do + response = internet.get("https://www.codeotaku.com/index", headers) + + expect(response).to be(:success?) + + response.close + end + + let(:sample) {{"hello" => "world"}} + let(:body) {[JSON.dump(sample)]} + + # This test is increasingly flakey. + it "can fetch remote json" do + response = internet.post("https://httpbin.org/anything", headers, body) + + expect(response).to be(:success?) + expect{JSON.parse(response.read)}.not.to raise_exception + end +end diff --git a/spec/async/http/internet/instance_spec.rb b/test/async/http/internet/instance.rb similarity index 59% rename from spec/async/http/internet/instance_spec.rb rename to test/async/http/internet/instance.rb index 846d2028..d254bd59 100644 --- a/spec/async/http/internet/instance_spec.rb +++ b/test/async/http/internet/instance.rb @@ -4,12 +4,11 @@ # Copyright, 2021-2023, by Samuel Williams. require 'async/http/internet/instance' -require 'async/reactor' -RSpec.describe Async::HTTP::Internet, timeout: 5 do +describe Async::HTTP::Internet do describe '.instance' do it "returns an internet instance" do - expect(Async::HTTP::Internet.instance).to be_kind_of(Async::HTTP::Internet) + expect(Async::HTTP::Internet.instance).to be_a(Async::HTTP::Internet) end end end diff --git a/spec/async/http/protocol/http10_spec.rb b/test/async/http/protocol/http10.rb similarity index 55% rename from spec/async/http/protocol/http10_spec.rb rename to test/async/http/protocol/http10.rb index 7f06eb98..26ae0be4 100644 --- a/spec/async/http/protocol/http10_spec.rb +++ b/test/async/http/protocol/http10.rb @@ -4,8 +4,8 @@ # Copyright, 2018-2023, by Samuel Williams. require 'async/http/protocol/http10' -require_relative 'shared_examples' +require 'async/http/a_protocol' -RSpec.describe Async::HTTP::Protocol::HTTP10 do - it_behaves_like Async::HTTP::Protocol +describe Async::HTTP::Protocol::HTTP10 do + it_behaves_like Async::HTTP::AProtocol end diff --git a/test/async/http/protocol/http11.rb b/test/async/http/protocol/http11.rb new file mode 100755 index 00000000..af2497ff --- /dev/null +++ b/test/async/http/protocol/http11.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2017-2023, by Samuel Williams. +# Copyright, 2018, by Janko Marohnić. +# Copyright, 2023, by Thomas Morgan. + +require 'async/http/protocol/http11' +require 'async/http/a_protocol' + +describe Async::HTTP::Protocol::HTTP11 do + it_behaves_like Async::HTTP::AProtocol + + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} + + with 'bad requests' do + def around + current = Console.logger.level + Console.logger.fatal! + + super + ensure + Console.logger.level = current + end + + it "should fail cleanly when path is empty" do + response = client.get("") + + expect(response.status).to be == 400 + end + end + + with 'head request' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, {}, ["Hello", "World"]] + end + end + + it "doesn't reply with body" do + 5.times do + response = client.head("/") + + expect(response).to be(:success?) + expect(response.version).to be == "HTTP/1.1" + expect(response.body).to be(:empty?) + + response.read + end + end + end + + with 'raw response' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + peer = request.hijack! + + peer.write( + "#{request.version} 200 It worked!\r\n" + + "connection: close\r\n" + + "\r\n" + + "Hello World!" + ) + peer.close + + nil + end + end + + it "reads raw response" do + response = client.get("/") + + expect(response.read).to be == "Hello World!" + end + end + end +end diff --git a/spec/async/http/protocol/http11/desync_spec.rb b/test/async/http/protocol/http11/desync.rb similarity index 73% rename from spec/async/http/protocol/http11/desync_spec.rb rename to test/async/http/protocol/http11/desync.rb index 5e9f7cf2..d31598a6 100644 --- a/spec/async/http/protocol/http11/desync_spec.rb +++ b/test/async/http/protocol/http11/desync.rb @@ -3,23 +3,26 @@ # Released under the MIT License. # Copyright, 2021-2023, by Samuel Williams. -require_relative '../../server_context' require 'async/http/protocol/http11' -RSpec.describe Async::HTTP::Protocol::HTTP11, timeout: 30 do - include_context Async::HTTP::Server +require 'sus/fixtures/async/http/server_context' + +describe Async::HTTP::Protocol::HTTP11 do + include Sus::Fixtures::Async::ReactorContext + include Sus::Fixtures::Async::HTTP::ServerContext - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + let(:app) do + Protocol::HTTP::Middleware.for do |request| Protocol::HTTP::Response[200, {}, [request.path]] end end - around do |example| + + def around current = Console.logger.level Console.logger.fatal! - - example.run + + super ensure Console.logger.level = current end @@ -63,7 +66,8 @@ child.stop end - puts "Backtraces" - pp backtraces.sort.uniq + # puts "Backtraces" + # pp backtraces.sort.uniq + expect(backtraces).not.to be(:empty?) end end diff --git a/test/async/http/protocol/http2.rb b/test/async/http/protocol/http2.rb new file mode 100644 index 00000000..f914653f --- /dev/null +++ b/test/async/http/protocol/http2.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. + +require 'async/http/protocol/http2' +require 'async/http/a_protocol' + +describe Async::HTTP::Protocol::HTTP2 do + it_behaves_like Async::HTTP::AProtocol + + with 'server' do + include Sus::Fixtures::Async::HTTP::ServerContext + let(:protocol) {subject} + + with 'bad requests' do + it "should fail with explicit authority" do + expect do + client.post("/", [[':authority', 'foo']]) + end.to raise_exception(Protocol::HTTP2::StreamError) + end + end + + with 'closed streams' do + it 'should delete stream after response stream is closed' do + response = client.get("/") + connection = response.connection + + response.read + + expect(connection.streams).to be(:empty?) + end + end + + with 'host header' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| + Protocol::HTTP::Response[200, request.headers, ["Authority: #{request.authority.inspect}"]] + end + end + + def make_client(endpoint, **options) + # We specify nil for the authority - it won't be sent. + options[:authority] = nil + super + end + + it "should not send :authority header if host header is present" do + response = client.post("/", [['host', 'foo']]) + + expect(response.headers).to have_keys('host') + expect(response.headers['host']).to be == 'foo' + + # TODO Should HTTP/2 respect host header? + expect(response.read).to be == "Authority: nil" + end + end + + with 'stopping requests' do + let(:notification) {Async::Notification.new} + + let(:app) do + Protocol::HTTP::Middleware.for do |request| + body = Async::HTTP::Body::Writable.new + + reactor.async do |task| + begin + 100.times do |i| + body.write("Chunk #{i}") + task.sleep (0.01) + end + rescue + # puts "Response generation failed: #{$!}" + ensure + body.close + notification.signal + end + end + + Protocol::HTTP::Response[200, {}, body] + end + end + + let(:pool) {client.pool} + + it "should close stream without closing connection" do + expect(pool).to be(:empty?) + + response = client.get("/") + + expect(pool).not.to be(:empty?) + + response.close + + notification.wait + + expect(response.stream.connection).to be(:reusable?) + end + end + end +end diff --git a/spec/async/http/proxy_spec.rb b/test/async/http/proxy.rb similarity index 75% rename from spec/async/http/proxy_spec.rb rename to test/async/http/proxy.rb index ace9665f..23c823e4 100644 --- a/spec/async/http/proxy_spec.rb +++ b/test/async/http/proxy.rb @@ -9,32 +9,34 @@ require 'async/http/protocol' require 'async/http/body/hijack' -require_relative 'server_context' +require 'sus/fixtures/async/http' -RSpec.shared_examples_for Async::HTTP::Proxy do - include_context Async::HTTP::Server +AProxy = Sus::Shared("a proxy") do + include Sus::Fixtures::Async::HTTP::ServerContext - describe '.proxied_endpoint' do + let(:protocol) {subject} + + with '.proxied_endpoint' do it "can construct valid endpoint" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_endpoint = client.proxied_endpoint(endpoint) - expect(proxied_endpoint).to be_kind_of(Async::HTTP::Endpoint) + expect(proxied_endpoint).to be_a(Async::HTTP::Endpoint) end end - describe '.proxied_client' do + with '.proxied_client' do it "can construct valid client" do endpoint = Async::HTTP::Endpoint.parse("http://www.codeotaku.com") proxied_client = client.proxied_client(endpoint) - expect(proxied_client).to be_kind_of(Async::HTTP::Client) + expect(proxied_client).to be_a(Async::HTTP::Client) end end - context 'CONNECT' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'CONNECT' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| chunk = stream.read stream.close_read @@ -52,7 +54,7 @@ response = client.connect("127.0.0.1:1234", [], input) - expect(response).to be_success + expect(response).to be(:success?) input.write(data) input.close @@ -61,9 +63,9 @@ end end - context 'echo server' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'echo server' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| expect(request.path).to be == "localhost:1" Async::HTTP::Body::Hijack.response(request, 200, {}) do |stream| @@ -81,7 +83,7 @@ it "can connect to remote system using block" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) proxy.connect do |peer| stream = Async::IO::Stream.new(peer) @@ -93,12 +95,12 @@ end proxy.close - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) end it "can connect to remote system" do proxy = Async::HTTP::Proxy.tcp(client, "localhost", 1) - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) stream = Async::IO::Stream.new(proxy.connect) @@ -110,13 +112,13 @@ stream.close proxy.close - expect(proxy.client.pool).to be_empty + expect(proxy.client.pool).to be(:empty?) end end - context 'proxied client' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'proxied client' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| expect(request.method).to be == "CONNECT" unless authorization_lambda.call(request) @@ -174,18 +176,16 @@ proxy_client = client.proxied_client(endpoint) response = proxy_client.get("/search") - expect(response).to_not be_failure + expect(response).not.to be(:failure?) # The response would be a redirect: - expect(response).to be_redirection + expect(response).to be(:redirection?) response.finish # The proxy.connnect response is not being released correctly - after pipe is done: - expect(proxy_client.pool).to_not be_empty + expect(proxy_client.pool).not.to be(:empty?) proxy_client.close - expect(proxy_client.pool).to be_empty - - pp client + expect(proxy_client.pool).to be(:empty?) end it 'can get secure website' do @@ -194,18 +194,18 @@ response = proxy_client.get("/search") - expect(response).to_not be_failure - expect(response.read).to_not be_empty + expect(response).not.to be(:failure?) + expect(response.read).not.to be(:empty?) proxy_client.close end - context 'authorization header required' do + with 'authorization header required' do let(:authorization_lambda) do ->(request) {request.headers['proxy-authorization'] == 'supersecretpassword' } end - context 'request includes headers' do + with 'request includes headers' do let(:headers) { [['Proxy-Authorization', 'supersecretpassword']] } it 'succeeds' do @@ -214,14 +214,14 @@ response = proxy_client.get('/search') - expect(response).to_not be_failure - expect(response.read).to_not be_empty + expect(response).not.to be(:failure?) + expect(response.read).not.to be(:empty?) proxy_client.close end end - context 'request does not include headers' do + with 'request does not include headers' do it 'does not succeed' do endpoint = Async::HTTP::Endpoint.parse("https://www.google.com") proxy_client = client.proxied_client(endpoint) @@ -229,7 +229,7 @@ expect do # Why is this response not 407? Because the response should come from the proxied connection, but that connection failed to be established. Because of that, there is no response. If we respond here with 407, it would be indistinguisable from the remote server returning 407. That would be an odd case, but none-the-less a valid one. response = proxy_client.get('/search') - end.to raise_error(Async::HTTP::Proxy::ConnectFailure) + end.to raise_exception(Async::HTTP::Proxy::ConnectFailure) proxy_client.close end @@ -238,14 +238,14 @@ end end -RSpec.describe Async::HTTP::Protocol::HTTP10 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP10 do + it_behaves_like AProxy end -RSpec.describe Async::HTTP::Protocol::HTTP11 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP11 do + it_behaves_like AProxy end -RSpec.describe Async::HTTP::Protocol::HTTP2 do - it_behaves_like Async::HTTP::Proxy +describe Async::HTTP::Protocol::HTTP2 do + it_behaves_like AProxy end diff --git a/spec/async/http/relative_location_spec.rb b/test/async/http/relative_location.rb similarity index 61% rename from spec/async/http/relative_location_spec.rb rename to test/async/http/relative_location.rb index 55657d39..4f453dbb 100644 --- a/spec/async/http/relative_location_spec.rb +++ b/test/async/http/relative_location.rb @@ -4,21 +4,20 @@ # Copyright, 2018-2023, by Samuel Williams. # Copyright, 2019-2020, by Brian Morearty. -require_relative 'server_context' - require 'async/http/relative_location' require 'async/http/server' -RSpec.describe Async::HTTP::RelativeLocation do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} +require 'sus/fixtures/async/http' + +describe Async::HTTP::RelativeLocation do + include Sus::Fixtures::Async::HTTP::ServerContext - subject {described_class.new(@client, 1)} + let(:relative_location) {subject.new(@client, 1)} - context 'server redirections' do - context '301' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with 'server redirections' do + with '301' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/home' Protocol::HTTP::Response[301, {'location' => '/'}, []] @@ -31,30 +30,30 @@ end it 'should redirect POST to GET' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "GET" end - context 'limiting redirects' do + with 'limiting redirects' do it 'should allow the maximum number of redirects' do - response = subject.get('/') + response = relative_location.get('/') response.finish - expect(response).to be_success + expect(response).to be(:success?) end it 'should fail with maximum redirects' do expect{ - response = subject.get('/home') - }.to raise_error(Async::HTTP::TooManyRedirects, /maximum/) + response = relative_location.get('/home') + }.to raise_exception(Async::HTTP::TooManyRedirects, message: be =~ /maximum/) end end end - context '302' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '302' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[302, {'location' => '/index.html'}, []] @@ -65,16 +64,16 @@ end it 'should redirect POST to GET' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "GET" end end - context '307' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '307' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[307, {'location' => '/index.html'}, []] @@ -85,16 +84,16 @@ end it 'should redirect with same method' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "POST" end end - context '308' do - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| + with '308' do + let(:app) do + Protocol::HTTP::Middleware.for do |request| case request.path when '/' Protocol::HTTP::Response[308, {'location' => '/index.html'}, []] @@ -105,9 +104,9 @@ end it 'should redirect with same method' do - response = subject.post('/') + response = relative_location.post('/') - expect(response).to be_success + expect(response).to be(:success?) expect(response.read).to be == "POST" end end diff --git a/test/async/http/retry.rb b/test/async/http/retry.rb new file mode 100644 index 00000000..20fb7dd8 --- /dev/null +++ b/test/async/http/retry.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2020-2023, by Samuel Williams. + +require 'async/http/client' +require 'async/http/endpoint' + +require 'sus/fixtures/async/http' + +describe 'consistent retry behaviour' do + include Sus::Fixtures::Async::HTTP::ServerContext + + let(:delay) {0.1} + let(:retries) {2} + + let(:app) do + Protocol::HTTP::Middleware.for do |request| + sleep(delay) + Protocol::HTTP::Response[200, {}, []] + end + end + + def make_request(body) + # This causes the first request to fail with "SocketError" which is retried: + Async::Task.current.with_timeout(delay / 2.0, SocketError) do + return client.get('/', {}, body) + end + end + + it "retries with nil body" do + response = make_request(nil) + expect(response).to be(:success?) + end + + it "retries with empty body" do + response = make_request([]) + expect(response).to be(:success?) + end +end diff --git a/test/async/http/ssl.rb b/test/async/http/ssl.rb new file mode 100644 index 00000000..54ccb70c --- /dev/null +++ b/test/async/http/ssl.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2018-2023, by Samuel Williams. + +require 'async/http/server' +require 'async/http/client' +require 'async/http/endpoint' + +require 'async/io/ssl_socket' + +require 'sus/fixtures/async' +require 'sus/fixtures/openssl' +require 'sus/fixtures/async/http' + +describe Async::HTTP::Server do + include Sus::Fixtures::Async::HTTP::ServerContext + include Sus::Fixtures::OpenSSL::ValidCertificateContext + + with "application layer protocol negotiation" do + let(:server_context) do + OpenSSL::SSL::SSLContext.new.tap do |context| + context.cert = certificate + + context.alpn_select_cb = lambda do |protocols| + protocols.last + end + + context.key = key + end + end + + let(:client_context) do + OpenSSL::SSL::SSLContext.new.tap do |context| + context.cert_store = certificate_store + + context.alpn_protocols = ["h2", "http/1.1"] + + context.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + end + + def make_server_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: server_context) + end + + def make_client_endpoint(bound_endpoint) + Async::IO::SSLEndpoint.new(super, ssl_context: client_context) + end + + it "client can get a resource via https" do + response = client.get("/") + + expect(response).to be(:success?) + expect(response.read).to be == "Hello World!" + end + end +end diff --git a/spec/async/http/statistics_spec.rb b/test/async/http/statistics.rb similarity index 56% rename from spec/async/http/statistics_spec.rb rename to test/async/http/statistics.rb index dac46248..df0aa039 100644 --- a/spec/async/http/statistics_spec.rb +++ b/test/async/http/statistics.rb @@ -3,17 +3,15 @@ # Released under the MIT License. # Copyright, 2018-2023, by Samuel Williams. -require_relative 'server_context' - require 'async/http/statistics' +require 'sus/fixtures/async/http' -RSpec.describe Async::HTTP::Statistics, timeout: 5 do - include_context Async::HTTP::Server - let(:protocol) {Async::HTTP::Protocol::HTTP1} +describe Async::HTTP::Statistics do + include Sus::Fixtures::Async::HTTP::ServerContext - let(:server) do - Async::HTTP::Server.for(@bound_endpoint) do |request| - statistics = described_class.start + let(:app) do + Protocol::HTTP::Middleware.for do |request| + statistics = subject.start response = Protocol::HTTP::Response[200, {}, ["Hello ", "World!"]] @@ -21,7 +19,7 @@ expect(statistics.sent).to be == 12 expect(error).to be_nil end.tap do |response| - expect(response.body).to receive(:complete_statistics).and_call_original + expect(response.body).to receive(:complete_statistics) end end end @@ -30,6 +28,6 @@ response = client.get("/") expect(response.read).to be == "Hello World!" - expect(response).to be_success + expect(response).to be(:success?) end end diff --git a/spec/rack/test_spec.rb b/test/rack/test.rb similarity index 87% rename from spec/rack/test_spec.rb rename to test/rack/test.rb index 3b12ddb2..0576ed77 100644 --- a/spec/rack/test_spec.rb +++ b/test/rack/test.rb @@ -3,14 +3,14 @@ # Released under the MIT License. # Copyright, 2019-2023, by Samuel Williams. +require 'sus/fixtures/async' +require 'async/http' + require 'rack/test' require 'rack/builder' -require 'async' -require 'async/http' - -RSpec.describe Rack::Test do - include_context Async::RSpec::Reactor +describe Rack::Test do + include Sus::Fixtures::Async::ReactorContext include Rack::Test::Methods let(:app) do