From 171ba2d7b855d9b166f7458033f9716cb2f8abd4 Mon Sep 17 00:00:00 2001 From: Zeke Gabrielse Date: Mon, 28 Oct 2024 15:49:36 -0500 Subject: [PATCH] add support for legacy index --- .../release_engines/pypi/simple_controller.rb | 2 +- .../rubygems/specs_controller.rb | 117 +++- app/controllers/concerns/compression.rb | 18 + app/models/release.rb | 4 + app/models/release_artifact.rb | 16 +- config/routes.rb | 7 +- .../api/v1/engines/rubygems/specs.feature | 656 +++++++++++++++++- features/step_definitions/request_steps.rb | 12 + 8 files changed, 790 insertions(+), 42 deletions(-) create mode 100644 app/controllers/concerns/compression.rb diff --git a/app/controllers/api/v1/release_engines/pypi/simple_controller.rb b/app/controllers/api/v1/release_engines/pypi/simple_controller.rb index 9fb34399de..fd591b997b 100644 --- a/app/controllers/api/v1/release_engines/pypi/simple_controller.rb +++ b/app/controllers/api/v1/release_engines/pypi/simple_controller.rb @@ -58,7 +58,7 @@ def set_package # users to set --index-url=, and we'll redirect non-existent # packages to PyPI for fulfillment. # - # TODO(ezekg) Make this configurable? + # TODO(ezekg) make this configurable? url = URI.parse("https://pypi.org/simple") pkg = CGI.escape(params[:package]) diff --git a/app/controllers/api/v1/release_engines/rubygems/specs_controller.rb b/app/controllers/api/v1/release_engines/rubygems/specs_controller.rb index 424e732866..85131ef61a 100644 --- a/app/controllers/api/v1/release_engines/rubygems/specs_controller.rb +++ b/app/controllers/api/v1/release_engines/rubygems/specs_controller.rb @@ -1,33 +1,126 @@ # frozen_string_literal: true +require 'rubygems/version' + module Api::V1::ReleaseEngines class Rubygems::SpecsController < Api::V1::BaseController + include Compression + before_action :scope_to_current_account! before_action :require_active_subscription! before_action :authenticate_with_token - before_action :set_artifact + before_action :set_packages, except: %i[quick_gemspec] + before_action :set_artifact, only: %i[quick_gemspec] - def quick + def quick_gemspec authorize! artifact, to: :show? - # rubygems expects a marshalled and zlib compressed gemspec - gemspec = artifact.specification.as_gemspec - serialized = Marshal.dump(gemspec) - compressed = Zlib::Deflate.deflate( - serialized, - ) + # rubygems expects marshalled and zlib compressed gemspec + gemspec = artifact.specification.as_gemspec + dumped = Marshal.dump(gemspec) + zipped = deflate(dumped) + + return unless + stale?(zipped, cache_control: { max_age: 1.day, private: true }) + + send_data zipped, filename: "#{params[:gem]}.gemspec.rz" + end + + def specs + authorize! packages, + to: :index? + + artifacts = authorized_scope(current_account.release_artifacts.unyanked.stable.for_packages(packages.ids)).preload(:specification, release: %i[product entitlements constraints]) + authorize! artifacts, + to: :index? + + # rubygems expects a marshalled and gzipped array of arrays + specs = to_specs(artifacts) + dumped = Marshal.dump(specs) + zipped = gzip(dumped) + + return unless + stale?(zipped, cache_control: { max_age: 1.day, private: true }) + + send_data zipped + end + + def latest_specs + authorize! packages, + to: :index? + + # use "distinct on" to select latest accessible version per-package and -platform + scoped_artifacts = authorized_scope(current_account.release_artifacts.unyanked.stable.for_packages(packages.ids)) + latest_artifacts = ReleaseArtifact.from( + scoped_artifacts.order_by_version.select('releases.release_package_id, release_artifacts.*'), + scoped_artifacts.table_name, + ) + .reorder(nil) # remove default order for "distinct on" + .distinct_on( + :release_package_id, + :release_platform_id, + ) + + artifacts = latest_artifacts.preload(:specification, release: %i[product entitlements constraints]) + authorize! artifacts, + to: :index? + + specs = to_specs(artifacts) + dumped = Marshal.dump(specs) + zipped = gzip(dumped) + + return unless + stale?(zipped, cache_control: { max_age: 1.day, private: true }) + + send_data zipped + end + + def prerelease_specs + authorize! packages, + to: :index? + + artifacts = authorized_scope(current_account.release_artifacts.unyanked.prerelease.for_packages(packages.ids)).preload(:specification, release: %i[product entitlements constraints]) + authorize! artifacts, + to: :index? + + specs = to_specs(artifacts) + dumped = Marshal.dump(specs) + zipped = gzip(dumped) - # for etag support return unless - stale?(compressed, cache_control: { max_age: 1.day, private: true }) + stale?(zipped, cache_control: { max_age: 1.day, private: true }) - send_data compressed, filename: "#{params[:gem]}.gemspec.rz" + send_data zipped end private - attr_reader :artifact + attr_reader :packages, + :artifact + + def to_specs(artifacts) + return [] unless artifacts.present? + + specs = artifacts.map do |artifact| + gemspec = artifact.specification.as_gemspec + + [gemspec.name, Gem::Version.new(gemspec.version), gemspec.platform.to_s] + end + + specs.sort_by(&:third) # platform + .sort_by(&:second) # version + .sort_by(&:first) # name + end + + def set_packages + @packages = authorized_scope(apply_scopes(current_account.release_packages.rubygems)) + .preload(:product) + .joins( + # we want to ignore packages without any eligible gem specs + releases: { artifacts: :specification }, + ) + end def set_artifact scoped_artifacts = authorized_scope(current_account.release_artifacts.gems) diff --git a/app/controllers/concerns/compression.rb b/app/controllers/concerns/compression.rb new file mode 100644 index 0000000000..e6ebde4f82 --- /dev/null +++ b/app/controllers/concerns/compression.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Compression + extend ActiveSupport::Concern + + def deflate(data, **) = Zlib::Deflate.deflate(data, **) + def gzip(data, deterministic: true, **) + zipped = StringIO.new + zipped.set_encoding(Encoding::BINARY) + + gz = Zlib::GzipWriter.new(zipped, Zlib::BEST_COMPRESSION) + gz.mtime = 0 if deterministic + gz.write(data) + gz.close + + zipped.string + end +end diff --git a/app/models/release.rb b/app/models/release.rb index 2e29441be1..3466ff2389 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -386,6 +386,10 @@ class Release < ApplicationRecord end } + scope :for_packages, -> packages { + joins(:package).where(package: { id: packages }) + } + scope :for_package, -> package { case package.presence when ReleasePackage, diff --git a/app/models/release_artifact.rb b/app/models/release_artifact.rb index d5309d4b5d..d89affe881 100644 --- a/app/models/release_artifact.rb +++ b/app/models/release_artifact.rb @@ -172,6 +172,7 @@ class ReleaseArtifact < ApplicationRecord scope :beta, -> { for_channel_key(%i(stable rc beta)) } scope :alpha, -> { for_channel_key(%i(stable rc beta alpha)) } scope :dev, -> { for_channel_key(%i(dev)) } + scope :prerelease, -> { for_channel_key(%i(rc beta alpha dev)) } scope :accessible_by, -> accessor { case accessor @@ -407,13 +408,14 @@ class ReleaseArtifact < ApplicationRecord scp = joins(release: { constraints: :entitlement }) scp = if strict - scp.reorder("#{table_name}.created_at": DEFAULT_SORT_ORDER) - .group(:id) - .having(<<~SQL.squish, codes:) - count(release_entitlement_constraints) = count(entitlements) filter ( - where entitlements.code in (:codes) - ) - SQL + agg = scp.reorder("#{table_name}.created_at": DEFAULT_SORT_ORDER) + .group(:id) + + agg.having(<<~SQL.squish, codes:) + count(release_entitlement_constraints) = count(entitlements) filter ( + where entitlements.code in (:codes) + ) + SQL else scp.where(entitlements: { code: codes }) end diff --git a/config/routes.rb b/config/routes.rb index 545c2dccfa..cde7f52ab5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,8 +81,11 @@ end scope module: :rubygems, defaults: { format: :binary } do - get 'quick/Marshal.4.8/:gem.gemspec.rz', to: 'specs#quick', as: :rubygems_quick_gemspec, constraints: { gem: /[^\/]+/ } - get 'gems/:gem.gem', to: 'gems#show', as: :rubygems_gem, constraints: { gem: /[^\/]+/ } + get 'quick/Marshal.4.8/:gem.gemspec.rz', to: 'specs#quick_gemspec', as: :rubygems_quick_gemspec, constraints: { gem: /[^\/]+/ } + get 'specs.4.8.gz', to: 'specs#specs', as: :rubygems_specs + get 'latest_specs.4.8.gz', to: 'specs#latest_specs', as: :rubygems_latest_specs + get 'prerelease_specs.4.8.gz', to: 'specs#prerelease_specs', as: :rubygems_prerelease_specs + get 'gems/:gem.gem', to: 'gems#show', as: :rubygems_gem, constraints: { gem: /[^\/]+/ } end end diff --git a/features/api/v1/engines/rubygems/specs.feature b/features/api/v1/engines/rubygems/specs.feature index ad07971e3f..e356f048f8 100644 --- a/features/api/v1/engines/rubygems/specs.feature +++ b/features/api/v1/engines/rubygems/specs.feature @@ -1,11 +1,15 @@ @api/v1 -Feature: Rubygems quick gemspec +Feature: Rubygems legacy index Background: Given the following "accounts" exist: | id | slug | name | | 14c038fd-b57e-432d-8c09-f50ebcd6a7bc | test1 | Test 1 | | b8cd8416-6dfb-44dd-9b69-1d73ee65baed | test2 | Test 2 | And the current account is "test1" + And the current account has the following "entitlement" rows: + | id | code | + | 1740e334-9d88-43c8-8b2e-38fd98f153d2 | JRUBY | + | 4cb07bd3-dc2d-4aed-b4e7-6d6b775e0005 | DEV | And the current account has the following "product" rows: | id | code | name | distribution_strategy | | 6198261a-48b5-4445-a045-9fed4afc7735 | test1 | Test 1 | LICENSED | @@ -20,20 +24,20 @@ Feature: Rubygems quick gemspec | 5666d47e-936e-4d48-8dd7-382d32462b4e | 6198261a-48b5-4445-a045-9fed4afc7735 | raw | quxx | | 3d771f82-a0ed-48fd-914a-f5ecda9b4044 | 6727d2a2-626c-4270-880c-3f7f378ea37a | rubygems | corge | And the current account has the following "release" rows: - | id | product_id | release_package_id | version | channel | status | - | 757e0a41-835e-42ad-bad8-84cabd29c72a | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.0.0 | stable | PUBLISHED | - | 3ff04fc6-9f10-4b84-b548-eb40f92ea331 | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.0.1 | stable | PUBLISHED | - | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.1.0 | stable | PUBLISHED | - | 972aa5b8-b12c-49f4-8ba4-7c9ae053dfa2 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.1 | beta | PUBLISHED | - | f36515f2-e907-40a3-ac81-2cc1042f8ec9 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.2 | beta | PUBLISHED | - | 56f66b77-f447-4300-828b-5cf92e457376 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.3 | beta | DRAFT | - | 0b5bb946-7346-448b-90a0-e8bbc02570e2 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | 7b113ac2-ae81-406a-b44e-f356126e2faa | 1.0.0 | stable | YANKED | - | 28a6e16d-c2a6-4be7-8578-e236182ee5c3 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | 7b113ac2-ae81-406a-b44e-f356126e2faa | 2.0.0 | stable | PUBLISHED | - | 00c9c981-8a75-494b-9207-71a829665729 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | cd46b4d3-60ab-43e9-b19d-87a9faf13adc | 1.0.0 | stable | PUBLISHED | - | e00475de-edcc-4571-adec-5ef1b91ddb85 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | cd46b4d3-60ab-43e9-b19d-87a9faf13adc | 1.1.0 | stable | PUBLISHED | - | d1bb5fca-0afc-4464-b321-4bd45cca8c7a | 6198261a-48b5-4445-a045-9fed4afc7735 | 5666d47e-936e-4d48-8dd7-382d32462b4e | 1.0.0 | stable | PUBLISHED | - | 70c40946-4b23-408c-aa1c-fa35421ff46a | 6198261a-48b5-4445-a045-9fed4afc7735 | 5666d47e-936e-4d48-8dd7-382d32462b4e | 1.1.0 | stable | PUBLISHED | - | 04d3d9da-4e91-4634-9aa0-41e39a23658c | 6198261a-48b5-4445-a045-9fed4afc7735 | | 0.0.1 | stable | PUBLISHED | + | id | product_id | release_package_id | version | channel | status | entitlements | + | 757e0a41-835e-42ad-bad8-84cabd29c72a | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.0.0 | stable | PUBLISHED | | + | 3ff04fc6-9f10-4b84-b548-eb40f92ea331 | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.0.1 | stable | PUBLISHED | | + | 028a38a2-0d17-4871-acb8-c5e6f040fc12 | 6198261a-48b5-4445-a045-9fed4afc7735 | 46e034fe-2312-40f8-bbeb-7d9957fb6fcf | 1.1.0 | stable | PUBLISHED | JRUBY | + | 972aa5b8-b12c-49f4-8ba4-7c9ae053dfa2 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.1 | beta | PUBLISHED | | + | f36515f2-e907-40a3-ac81-2cc1042f8ec9 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.2 | beta | PUBLISHED | DEV | + | 56f66b77-f447-4300-828b-5cf92e457376 | 6198261a-48b5-4445-a045-9fed4afc7735 | 2f8af04a-2424-4ca2-8480-6efe24318d1a | 1.0.0-beta.3 | beta | DRAFT | | + | 0b5bb946-7346-448b-90a0-e8bbc02570e2 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | 7b113ac2-ae81-406a-b44e-f356126e2faa | 1.0.0 | stable | YANKED | | + | 28a6e16d-c2a6-4be7-8578-e236182ee5c3 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | 7b113ac2-ae81-406a-b44e-f356126e2faa | 2.0.0 | stable | PUBLISHED | | + | 00c9c981-8a75-494b-9207-71a829665729 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | cd46b4d3-60ab-43e9-b19d-87a9faf13adc | 1.0.0 | stable | PUBLISHED | | + | e00475de-edcc-4571-adec-5ef1b91ddb85 | cad3c65c-b6a5-4b3d-bce6-c2280953b8b8 | cd46b4d3-60ab-43e9-b19d-87a9faf13adc | 1.1.0 | stable | PUBLISHED | | + | d1bb5fca-0afc-4464-b321-4bd45cca8c7a | 6198261a-48b5-4445-a045-9fed4afc7735 | 5666d47e-936e-4d48-8dd7-382d32462b4e | 1.0.0 | stable | PUBLISHED | | + | 70c40946-4b23-408c-aa1c-fa35421ff46a | 6198261a-48b5-4445-a045-9fed4afc7735 | 5666d47e-936e-4d48-8dd7-382d32462b4e | 1.1.0 | stable | PUBLISHED | | + | 04d3d9da-4e91-4634-9aa0-41e39a23658c | 6198261a-48b5-4445-a045-9fed4afc7735 | | 0.0.1 | stable | PUBLISHED | | And the current account has the following "artifact" rows: | id | release_id | filename | filetype | platform | checksum | status | | 5762c549-7f5b-4a73-9873-3acdb1213fe8 | 757e0a41-835e-42ad-bad8-84cabd29c72a | foo-1.0.0.gem | gem | ruby | 32eae8a165580f793a2fde46dd9ff218bb490ee3d1aeda368dfee7e3726ffb67 | UPLOADED | @@ -68,7 +72,8 @@ Feature: Rubygems quick gemspec Accept: application/octet-stream """ - Scenario: Endpoint should be inaccessible when account is disabled + # quick gemspec + Scenario: Endpoint should be inaccessible when account is disabled (quick gemspec) Given the account "test1" is canceled And I am an admin of account "test1" And I use an authentication token @@ -228,7 +233,7 @@ Feature: Rubygems quick gemspec And the current account has 1 "license" for the last "policy" And I am a license of account "test1" And I authenticate with my key - When I send a GET request to "/accounts/test1/engines/rubygems/quick/Marshal.4.8/foo-1.1.0.gemspec.rz" + When I send a GET request to "/accounts/test1/engines/rubygems/quick/Marshal.4.8/foo-1.0.1.gemspec.rz" Then the response status should be "200" And the response should contain the following headers: """ @@ -237,11 +242,11 @@ Feature: Rubygems quick gemspec And the response body should be a gemspec with the following content: """ # -*- encoding: utf-8 -*- - # stub: foo 1.1.0 ruby lib + # stub: foo 1.0.1 ruby lib Gem::Specification.new do |s| s.name = "foo".freeze - s.version = "1.1.0".freeze + s.version = "1.0.1".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] @@ -271,7 +276,7 @@ Feature: Rubygems quick gemspec And the current account has 1 "license" for the last "policy" And I am a license of account "test1" And I authenticate with my key - When I send a GET request to "/accounts/test1/engines/rubygems/quick/Marshal.4.8/foo-1.1.0.gemspec.rz" + When I send a GET request to "/accounts/test1/engines/rubygems/quick/Marshal.4.8/foo-1.0.1.gemspec.rz" Then the response status should be "404" Scenario: License retrieves an open gem (different product) @@ -474,3 +479,614 @@ Feature: Rubygems quick gemspec s.add_runtime_dependency(%q.freeze, [">= 0".freeze]) end """ + + # specs + Scenario: Endpoint should be inaccessible when account is disabled (specs) + Given the account "test1" is canceled + And I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "403" + And the response should contain the following headers: + """ + { "Content-Type": "application/json; charset=utf-8" } + """ + + Scenario: Endpoint should return stable gem specs + Given I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: Endpoint should support etags (match) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"8b42f6df6c6f86a8fb92d0a6d6d1f3f0" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "304" + + Scenario: Endpoint should support etags (mismatch) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"foo" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following raw headers: + """ + Etag: W/"8b42f6df6c6f86a8fb92d0a6d6d1f3f0" + Cache-Control: max-age=86400, private + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: Product retrieves their stable gem specs + Given I am product "test1" of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: License retrieves their stable gem specs (entitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: License retrieves their stable gem specs (unentitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their stable gem specs (with entitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their stable gem specs (with unentitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their stable gem specs (with entitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their stable gem specs (with unentitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: Anon retrieves stable gem specs + When I send a GET request to "/accounts/test1/engines/rubygems/specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"]] + """ + + # latest specs + Scenario: Endpoint should be inaccessible when account is disabled (latest specs) + Given the account "test1" is canceled + And I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "403" + And the response should contain the following headers: + """ + { "Content-Type": "application/json; charset=utf-8" } + """ + + Scenario: Endpoint should return latest gem specs + Given I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: Endpoint should support etags (match) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"d4b09b006db58a9640fd6ef6c7fe5ef2" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "304" + + Scenario: Endpoint should support etags (mismatch) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"foo" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following raw headers: + """ + Etag: W/"d4b09b006db58a9640fd6ef6c7fe5ef2" + Cache-Control: max-age=86400, private + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: Product retrieves their latest gem specs + Given I am product "test1" of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: License retrieves their latest gem specs (entitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: License retrieves their latest gem specs (unentitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their latest gem specs (with entitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their latest gem specs (with unentitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their latest gem specs (with entitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the first "entitlement" and the last "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "java"], ["foo", #, "ruby"]] + """ + + Scenario: User retrieves their latest gem specs (with unentitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"], ["foo", #, "ruby"]] + """ + + Scenario: Anon retrieves latest gem specs + When I send a GET request to "/accounts/test1/engines/rubygems/latest_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["baz", #, "ruby"]] + """ + + # prerelease + Scenario: Endpoint should be inaccessible when account is disabled (prerelease specs) + Given the account "test1" is canceled + And I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "403" + And the response should contain the following headers: + """ + { "Content-Type": "application/json; charset=utf-8" } + """ + Scenario: Endpoint should return prerelease gem specs + Given I am an admin of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: Endpoint should support etags (match) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"d2960385683d83987661b81ddbd74a80" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "304" + + Scenario: Endpoint should support etags (mismatch) + Given I am an admin of account "test1" + And I use an authentication token + And I send the following raw headers: + """ + If-None-Match: W/"foo" + """ + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following raw headers: + """ + Etag: W/"d2960385683d83987661b81ddbd74a80" + Cache-Control: max-age=86400, private + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: Product retrieves their prerelease gem specs + Given I am product "test1" of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: License retrieves their prerelease gem specs (entitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the second "entitlement" and the last "license" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: License retrieves their prerelease gem specs (unentitled) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "license" for the last "policy" + And I am a license of account "test1" + And I authenticate with my key + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"]] + """ + + Scenario: User retrieves their prerelease gem specs (with entitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And the current account has 1 "license-entitlement" for the second "entitlement" and the last "license" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: User retrieves their prerelease gem specs (with unentitled owned license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" and the last "user" as "owner" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"]] + """ + + Scenario: User retrieves their prerelease gem specs (with entitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-entitlement" for the second "entitlement" and the last "license" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"], ["bar", #, "ruby"]] + """ + + Scenario: User retrieves their prerelease gem specs (with unentitled license) + Given the current account has 1 "policy" for the first "product" with the following: + """ + { "authenticationStrategy": "LICENSE" } + """ + And the current account has 1 "user" + And the current account has 1 "license" for the last "policy" + And the current account has 1 "license-user" for the last "license" and the last "user" + And I am the last user of account "test1" + And I use an authentication token + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [["bar", #, "ruby"]] + """ + + Scenario: Anon retrieves prerelease gem specs + When I send a GET request to "/accounts/test1/engines/rubygems/prerelease_specs.4.8.gz" + Then the response status should be "200" + And the response should contain the following headers: + """ + { "Content-Type": "application/octet-stream" } + """ + And the response body should be gemspecs with the following content: + """ + [] + """ diff --git a/features/step_definitions/request_steps.rb b/features/step_definitions/request_steps.rb index 8f9de653ab..c1d6c3a3bc 100644 --- a/features/step_definitions/request_steps.rb +++ b/features/step_definitions/request_steps.rb @@ -1260,6 +1260,18 @@ expect(gemspec.strip).to eq body.strip end +Then /^the response body should be gemspecs with the following content:$/ do |body| + body = parse_placeholders(body, account: @account, bearer: @bearer, crypt: @crypt) + + gz = Zlib::GzipReader.new(StringIO.new(last_response.body.strip)) + decompressed = gz.read + deserialized = Marshal.load(decompressed) + specs = deserialized.inspect + + # FIXME(ezekg) can we use prism to clean this up? + expect(specs).to eq body.strip +end + Given /^the JSON data should be sorted by "([^\"]+)"$/ do |key| data = JSON.parse(last_response.body) .fetch('data')