paquette 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.md +10 -0
- data/README.md +90 -0
- data/lib/paquette/gem_server/directory_gem_repository.rb +154 -0
- data/lib/paquette/gem_server/gem_repacker.rb +170 -0
- data/lib/paquette/gem_server/gem_repository.rb +53 -0
- data/lib/paquette/gem_server/personalizer.rb +66 -0
- data/lib/paquette/gem_server/read_gated_repository.rb +74 -0
- data/lib/paquette/gem_server.rb +393 -0
- data/lib/paquette/npm_repacker.rb +153 -0
- data/lib/paquette/npm_server/directory_npm_repository.rb +127 -0
- data/lib/paquette/npm_server/gated_npm_repository.rb +66 -0
- data/lib/paquette/npm_server/npm_repository.rb +53 -0
- data/lib/paquette/npm_server.rb +98 -0
- data/lib/paquette/routes.rb +78 -0
- data/lib/paquette/subdomain_router.rb +59 -0
- data/lib/paquette/version.rb +3 -0
- data/lib/paquette.rb +12 -0
- data/test/fixtures/gems/minuscule_test/minuscule_test-0.1.0.gem +0 -0
- data/test/fixtures/gems/zip_kit/zip_kit-6.2.0.gem +0 -0
- data/test/fixtures/gems/zip_kit/zip_kit-6.2.1.gem +0 -0
- data/test/fixtures/npm/react-dropzone/react-dropzone-14.3.8.tgz +0 -0
- data/test/gem_server/bundler_install_test.rb +86 -0
- data/test/gem_server/directory_gem_repository_test.rb +208 -0
- data/test/gem_server/gem_repacker_test.rb +289 -0
- data/test/gem_server/personalizer_metadata_test.rb +49 -0
- data/test/gem_server/personalizer_test.rb +66 -0
- data/test/gem_server/read_gated_repository_test.rb +254 -0
- data/test/gem_server/repository_stack_test.rb +71 -0
- data/test/gem_server_test.rb +339 -0
- data/test/npm_repacker_sourcemap_test.rb +108 -0
- data/test/npm_repacker_test.rb +117 -0
- data/test/npm_server_test.rb +140 -0
- data/test/routes_test.rb +142 -0
- data/test/subdomain_router_integration_test.rb +126 -0
- data/test/subdomain_router_test.rb +125 -0
- data/test/test_helper.rb +12 -0
- metadata +117 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "delegate"
|
|
2
|
+
|
|
3
|
+
module Paquette
|
|
4
|
+
class NpmServer
|
|
5
|
+
class GatedNpmRepository < SimpleDelegator
|
|
6
|
+
def initialize(repository, &entitler)
|
|
7
|
+
super(repository)
|
|
8
|
+
@entitler = entitler
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def package_names
|
|
12
|
+
super.select { |name| @entitler.call(name: name) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def package_versions
|
|
16
|
+
super.select do |package_name, version|
|
|
17
|
+
@entitler.call(name: package_name, version: version)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def versions_for_package(package_name)
|
|
22
|
+
return [] unless @entitler.call(name: package_name)
|
|
23
|
+
super.select do |version|
|
|
24
|
+
@entitler.call(name: package_name, version: version)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def package_file_path(package_name, version)
|
|
29
|
+
if @entitler.call(name: package_name, version: version)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def package_metadata(package_name)
|
|
35
|
+
return nil unless @entitler.call(name: package_name)
|
|
36
|
+
|
|
37
|
+
# Get the metadata from the underlying repository, but filter by entitled versions
|
|
38
|
+
metadata = super
|
|
39
|
+
return metadata unless metadata
|
|
40
|
+
|
|
41
|
+
# Filter versions based on entitlements
|
|
42
|
+
entitled_versions = versions_for_package(package_name)
|
|
43
|
+
|
|
44
|
+
# Update the versions hash to only include entitled versions
|
|
45
|
+
metadata[:versions] = metadata[:versions].slice(*entitled_versions)
|
|
46
|
+
|
|
47
|
+
# Update time entries to only include entitled versions
|
|
48
|
+
metadata[:time] = metadata[:time].slice(*entitled_versions)
|
|
49
|
+
|
|
50
|
+
# Update dist-tags to use the latest entitled version
|
|
51
|
+
if entitled_versions.any?
|
|
52
|
+
latest_entitled = entitled_versions.max_by { |v| Gem::Version.new(v) }
|
|
53
|
+
metadata[:"dist-tags"][:latest] = latest_entitled
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
metadata
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def package_exists?(package_name, version)
|
|
60
|
+
if @entitler.call(name: package_name, version: version)
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Paquette
|
|
5
|
+
class NpmServer
|
|
6
|
+
# Abstract base class for NPM package repositories
|
|
7
|
+
class NpmRepository
|
|
8
|
+
def initialize
|
|
9
|
+
raise NotImplementedError, "NpmRepository is an abstract class"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns an array of package names available in the repository
|
|
13
|
+
def package_names
|
|
14
|
+
raise NotImplementedError, "Subclasses must implement package_names"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns an array of [name, version] pairs for all packages
|
|
18
|
+
def package_versions
|
|
19
|
+
raise NotImplementedError, "Subclasses must implement package_versions"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns an array of versions for a specific package
|
|
23
|
+
def versions_for_package(package_name)
|
|
24
|
+
raise NotImplementedError, "Subclasses must implement versions_for_package"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the package file path for a specific package and version
|
|
28
|
+
def package_file_path(package_name, version)
|
|
29
|
+
raise NotImplementedError, "Subclasses must implement package_file_path"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns whether a package file exists for the given name and version
|
|
33
|
+
def package_exists?(package_name, version)
|
|
34
|
+
raise NotImplementedError, "Subclasses must implement package_exists?"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the package.json for a specific package and version
|
|
38
|
+
def package_info(package_name, version)
|
|
39
|
+
raise NotImplementedError, "Subclasses must implement package_info"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns dependencies for a specific package and version
|
|
43
|
+
def package_dependencies(package_name, version)
|
|
44
|
+
raise NotImplementedError, "Subclasses must implement package_dependencies"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the full package metadata for NPM registry format
|
|
48
|
+
def package_metadata(package_name)
|
|
49
|
+
raise NotImplementedError, "Subclasses must implement package_metadata"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require_relative "routes"
|
|
4
|
+
require_relative "npm_server/directory_npm_repository"
|
|
5
|
+
require_relative "npm_server/gated_npm_repository"
|
|
6
|
+
|
|
7
|
+
module Paquette
|
|
8
|
+
class NpmServer
|
|
9
|
+
@@routes = Routes.draw do |r|
|
|
10
|
+
# Root endpoint
|
|
11
|
+
r.get "/" do
|
|
12
|
+
text_ok("Paquette NPM Repository")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# NPM API endpoints
|
|
16
|
+
r.get "/-/ping" do
|
|
17
|
+
json_ok({})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
r.get "/-/whoami" do
|
|
21
|
+
json_ok({username: "paquette"})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Dynamic endpoints with parameters
|
|
25
|
+
r.get "/-/package/:package_name/dist-tags" do |package_name:|
|
|
26
|
+
metadata = @repository.package_metadata(package_name)
|
|
27
|
+
if metadata && metadata[:"dist-tags"]
|
|
28
|
+
json_ok(metadata[:"dist-tags"])
|
|
29
|
+
else
|
|
30
|
+
not_found("Package not found")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
r.get "/:package_name" do |package_name:|
|
|
35
|
+
metadata = @repository.package_metadata(package_name)
|
|
36
|
+
if metadata
|
|
37
|
+
json_ok(metadata)
|
|
38
|
+
else
|
|
39
|
+
not_found("Package not found")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
r.get "/:package_name/:tarball_name" do |package_name:, tarball_name:|
|
|
44
|
+
# Extract version from tarball name (e.g., "package-1.0.0.tgz")
|
|
45
|
+
if (match = tarball_name.match(/^#{Regexp.escape(package_name)}-(\d+\.\d+\.\d+.*)\.tgz$/))
|
|
46
|
+
version = match[1]
|
|
47
|
+
package_path = @repository.package_file_path(package_name, version)
|
|
48
|
+
|
|
49
|
+
if @repository.package_exists?(package_name, version)
|
|
50
|
+
[200, {"Content-Type" => "application/octet-stream"}, [File.read(package_path)]]
|
|
51
|
+
else
|
|
52
|
+
not_found("Package not found")
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
not_found("Invalid package filename")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(packages_dir)
|
|
61
|
+
@dir_repository = DirectoryNpmRepository.new(packages_dir)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(env)
|
|
65
|
+
request = Rack::Request.new(env)
|
|
66
|
+
@repository = GatedNpmRepository.new(@dir_repository) { |name:, version: nil| true }
|
|
67
|
+
|
|
68
|
+
if (route = @@routes.match(request))
|
|
69
|
+
@@routes.perform_action(route, self, request)
|
|
70
|
+
else
|
|
71
|
+
not_found("Not Found")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Helper methods for common response patterns
|
|
78
|
+
def json_ok(data)
|
|
79
|
+
[200, {"Content-Type" => "application/json"}, [JSON.pretty_generate(data)]]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def text_ok(data)
|
|
83
|
+
[200, {"Content-Type" => "text/plain"}, [data]]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def not_found(message = "Not Found")
|
|
87
|
+
[404, {"Content-Type" => "text/plain"}, [message]]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def bad_request(message = "Bad Request")
|
|
91
|
+
[400, {"Content-Type" => "text/plain"}, [message]]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def server_error(message = "Internal Server Error")
|
|
95
|
+
[500, {"Content-Type" => "text/plain"}, [message]]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "mustermann"
|
|
2
|
+
|
|
3
|
+
module Paquette
|
|
4
|
+
class Routes
|
|
5
|
+
class Route
|
|
6
|
+
attr_reader :method, :pattern, :block
|
|
7
|
+
|
|
8
|
+
def initialize(method, pattern, block)
|
|
9
|
+
@method = method
|
|
10
|
+
@pattern = Mustermann.new(pattern)
|
|
11
|
+
@block = block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def match?(request)
|
|
15
|
+
@method == request.request_method && @pattern.match(request.path_info)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def params(request)
|
|
19
|
+
@pattern.params(request.path_info)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def perform_action(instance, request)
|
|
23
|
+
# Extract params and pass them as keyword arguments
|
|
24
|
+
route_params = params(request)
|
|
25
|
+
# Also include query parameters
|
|
26
|
+
query_params = request.params
|
|
27
|
+
# Convert string keys to symbols for keyword arguments
|
|
28
|
+
symbol_params = route_params.merge(query_params).transform_keys(&:to_sym)
|
|
29
|
+
instance.instance_exec(**symbol_params, &@block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class RouteBuilder
|
|
34
|
+
def initialize(routes)
|
|
35
|
+
@routes = routes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get(pattern, &block)
|
|
39
|
+
@routes << Route.new("GET", pattern, block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def post(pattern, &block)
|
|
43
|
+
@routes << Route.new("POST", pattern, block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def put(pattern, &block)
|
|
47
|
+
@routes << Route.new("PUT", pattern, block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete(pattern, &block)
|
|
51
|
+
@routes << Route.new("DELETE", pattern, block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def patch(pattern, &block)
|
|
55
|
+
@routes << Route.new("PATCH", pattern, block)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.draw(&block)
|
|
60
|
+
routes = []
|
|
61
|
+
builder = RouteBuilder.new(routes)
|
|
62
|
+
block.call(builder)
|
|
63
|
+
new(routes)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(routes)
|
|
67
|
+
@routes = routes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def match(request)
|
|
71
|
+
@routes.find { |route| route.match?(request) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def perform_action(route, instance, request)
|
|
75
|
+
route.perform_action(instance, request)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative "gem_server"
|
|
2
|
+
require_relative "npm_server"
|
|
3
|
+
|
|
4
|
+
module Paquette
|
|
5
|
+
class SubdomainRouter
|
|
6
|
+
def initialize(&block)
|
|
7
|
+
@mappings = {}
|
|
8
|
+
@fallback = nil
|
|
9
|
+
|
|
10
|
+
if block_given?
|
|
11
|
+
block.call(self)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
request = Rack::Request.new(env)
|
|
17
|
+
host = request.host
|
|
18
|
+
|
|
19
|
+
# Extract subdomain from host
|
|
20
|
+
subdomain = extract_subdomain(host)
|
|
21
|
+
|
|
22
|
+
if subdomain && @mappings[subdomain]
|
|
23
|
+
app = @mappings[subdomain]
|
|
24
|
+
if app.respond_to?(:call)
|
|
25
|
+
app.call(env)
|
|
26
|
+
else
|
|
27
|
+
# If it's a class, instantiate it
|
|
28
|
+
app.new.call(env)
|
|
29
|
+
end
|
|
30
|
+
elsif @fallback
|
|
31
|
+
@fallback.call(env)
|
|
32
|
+
else
|
|
33
|
+
[404, {}, ["Subdomain not found"]]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def map(subdomain, to:)
|
|
38
|
+
@mappings[subdomain] = to
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fallback(to:)
|
|
42
|
+
@fallback = to
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def extract_subdomain(host)
|
|
48
|
+
# Handle localhost with port (e.g., localhost:9292)
|
|
49
|
+
host = host.split(":").first if host.include?(":")
|
|
50
|
+
|
|
51
|
+
# Extract the first part of the hostname (subdomain)
|
|
52
|
+
if (match = host.match(/^([a-z\-\d]+)\./))
|
|
53
|
+
subdomain = match[1]
|
|
54
|
+
# Only return if this subdomain is actually mapped
|
|
55
|
+
@mappings.key?(subdomain) ? subdomain : nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/paquette.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Paquette
|
|
2
|
+
require_relative "paquette/version"
|
|
3
|
+
|
|
4
|
+
require_relative "paquette/routes"
|
|
5
|
+
require_relative "paquette/gem_server/gem_repository"
|
|
6
|
+
require_relative "paquette/gem_server/directory_gem_repository"
|
|
7
|
+
require_relative "paquette/gem_server"
|
|
8
|
+
require_relative "paquette/npm_server"
|
|
9
|
+
require_relative "paquette/subdomain_router"
|
|
10
|
+
require_relative "paquette/gem_server/gem_repacker"
|
|
11
|
+
require_relative "paquette/npm_repacker"
|
|
12
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
require "socket"
|
|
3
|
+
|
|
4
|
+
class BundlerInstallTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
@server_thread = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def teardown
|
|
10
|
+
stop_server if @server_thread
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_gem_installation
|
|
14
|
+
start_server
|
|
15
|
+
sleep 2
|
|
16
|
+
# wait_for_server
|
|
17
|
+
|
|
18
|
+
tempdir = Dir.mktmpdir
|
|
19
|
+
pid = fork do
|
|
20
|
+
Dir.chdir(tempdir)
|
|
21
|
+
File.open("Gemfile", "w") do |gemfile|
|
|
22
|
+
gemfile << <<~RUBY
|
|
23
|
+
source "http://gem.localhost:#{@server_port}" do
|
|
24
|
+
gem "zip_kit"
|
|
25
|
+
gem "minuscule_test"
|
|
26
|
+
end
|
|
27
|
+
RUBY
|
|
28
|
+
end
|
|
29
|
+
Bundler.with_unbundled_env do
|
|
30
|
+
ENV["BUNDLE_GEMFILE"] = Dir.pwd + "/Gemfile"
|
|
31
|
+
system("bundle config set --local disable_checksum_validation true")
|
|
32
|
+
system("bundle install --verbose")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
Process.wait(pid)
|
|
36
|
+
ensure
|
|
37
|
+
stop_server
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def find_free_port
|
|
43
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
44
|
+
port = server.addr[1]
|
|
45
|
+
server.close
|
|
46
|
+
port
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def start_server
|
|
50
|
+
return if @server_thread
|
|
51
|
+
@server_port = find_free_port
|
|
52
|
+
env = {"PAQUETTE_PACKAGES_DIR" => File.expand_path("../fixtures", __dir__)}
|
|
53
|
+
cmd = "puma --port #{@server_port} -e test"
|
|
54
|
+
|
|
55
|
+
@server_thread ||= Thread.new { system(env, cmd) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stop_server
|
|
59
|
+
return unless @server_thread
|
|
60
|
+
|
|
61
|
+
# Kill the puma process
|
|
62
|
+
system("pkill -f 'puma.*#{@server_port}'")
|
|
63
|
+
@server_thread.kill
|
|
64
|
+
@server_thread.join(1) # Wait up to 1 second for thread to finish
|
|
65
|
+
@server_thread = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def wait_for_server
|
|
69
|
+
max_attempts = 50 # 5 seconds with 0.1 second intervals
|
|
70
|
+
attempts = 0
|
|
71
|
+
|
|
72
|
+
while attempts < max_attempts
|
|
73
|
+
begin
|
|
74
|
+
response = Net::HTTP.get_response(URI("http://127.0.0.1:#{@server_port}/"))
|
|
75
|
+
return if response.code == "200"
|
|
76
|
+
rescue
|
|
77
|
+
# Server not ready yet
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sleep 0.1
|
|
81
|
+
attempts += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
flunk "Server did not start within 5 seconds"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
|
|
3
|
+
class DirectoryGemRepositoryTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@gems_dir = FIXTURE_GEMS_DIR
|
|
6
|
+
@repository = Paquette::GemServer::DirectoryGemRepository.new(@gems_dir)
|
|
7
|
+
@minuscule_fixture = File.join(@gems_dir, "minuscule_test", "minuscule_test-0.1.0.gem")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_gem_names
|
|
11
|
+
names = @repository.gem_names
|
|
12
|
+
assert_equal ["minuscule_test", "zip_kit"], names.sort
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_gem_versions
|
|
16
|
+
versions = @repository.gem_versions
|
|
17
|
+
assert_equal 3, versions.length
|
|
18
|
+
|
|
19
|
+
# standard:disable Style/HashSlice
|
|
20
|
+
zip_kit_versions = versions.select { |name, _| name == "zip_kit" }
|
|
21
|
+
# standard:enable Style/HashSlice
|
|
22
|
+
assert_equal 2, zip_kit_versions.length
|
|
23
|
+
assert_includes zip_kit_versions.map { |_, v| v }, "6.2.0"
|
|
24
|
+
assert_includes zip_kit_versions.map { |_, v| v }, "6.2.1"
|
|
25
|
+
|
|
26
|
+
# standard:disable Style/HashSlice
|
|
27
|
+
minuscule_versions = versions.select { |name, _| name == "minuscule_test" }
|
|
28
|
+
# standard:enable Style/HashSlice
|
|
29
|
+
assert_equal 1, minuscule_versions.length
|
|
30
|
+
assert_includes minuscule_versions.map { |_, v| v }, "0.1.0"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_versions_for_gem
|
|
34
|
+
zip_kit_versions = @repository.versions_for_gem("zip_kit")
|
|
35
|
+
assert_equal 2, zip_kit_versions.length
|
|
36
|
+
assert_includes zip_kit_versions, "6.2.0"
|
|
37
|
+
assert_includes zip_kit_versions, "6.2.1"
|
|
38
|
+
|
|
39
|
+
minuscule_versions = @repository.versions_for_gem("minuscule_test")
|
|
40
|
+
assert_equal ["0.1.0"], minuscule_versions
|
|
41
|
+
|
|
42
|
+
assert_equal [], @repository.versions_for_gem("nonexistent")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_gem_exists
|
|
46
|
+
assert @repository.gem_exists?("zip_kit", "6.2.0")
|
|
47
|
+
assert @repository.gem_exists?("zip_kit", "6.2.1")
|
|
48
|
+
assert @repository.gem_exists?("minuscule_test", "0.1.0")
|
|
49
|
+
|
|
50
|
+
refute @repository.gem_exists?("zip_kit", "6.3.2")
|
|
51
|
+
refute @repository.gem_exists?("nonexistent", "1.0.0")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_gem_file_path
|
|
55
|
+
assert_equal File.join(@gems_dir, "zip_kit", "zip_kit-6.2.0.gem"),
|
|
56
|
+
@repository.gem_file_path("zip_kit", "6.2.0")
|
|
57
|
+
assert_equal File.join(@gems_dir, "minuscule_test", "minuscule_test-0.1.0.gem"),
|
|
58
|
+
@repository.gem_file_path("minuscule_test", "0.1.0")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_gem_spec
|
|
62
|
+
spec = @repository.gem_spec("zip_kit", "6.2.0")
|
|
63
|
+
refute_nil spec
|
|
64
|
+
assert_equal "zip_kit", spec.name
|
|
65
|
+
assert_equal "6.2.0", spec.version.to_s
|
|
66
|
+
|
|
67
|
+
spec = @repository.gem_spec("minuscule_test", "0.1.0")
|
|
68
|
+
refute_nil spec
|
|
69
|
+
assert_equal "minuscule_test", spec.name
|
|
70
|
+
assert_equal "0.1.0", spec.version.to_s
|
|
71
|
+
|
|
72
|
+
assert_nil @repository.gem_spec("nonexistent", "1.0.0")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_gem_dependencies
|
|
76
|
+
assert @repository.gem_dependencies("zip_kit", "6.2.0").is_a?(Array)
|
|
77
|
+
assert @repository.gem_dependencies("minuscule_test", "0.1.0").is_a?(Array)
|
|
78
|
+
assert_equal [], @repository.gem_dependencies("nonexistent", "1.0.0")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_add_gem_persists_file_and_returns_spec
|
|
82
|
+
binary = File.binread(@minuscule_fixture)
|
|
83
|
+
|
|
84
|
+
Dir.mktmpdir do |tmp|
|
|
85
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
86
|
+
|
|
87
|
+
spec = repo.add_gem(binary)
|
|
88
|
+
|
|
89
|
+
assert_equal "minuscule_test", spec.name
|
|
90
|
+
assert_equal "0.1.0", spec.version.to_s
|
|
91
|
+
assert repo.gem_exists?("minuscule_test", "0.1.0")
|
|
92
|
+
assert_equal binary, File.binread(repo.gem_file_path("minuscule_test", "0.1.0"))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_add_gem_rejects_duplicate
|
|
97
|
+
binary = File.binread(@minuscule_fixture)
|
|
98
|
+
|
|
99
|
+
Dir.mktmpdir do |tmp|
|
|
100
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
101
|
+
repo.add_gem(binary)
|
|
102
|
+
|
|
103
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::GemAlreadyExists) do
|
|
104
|
+
repo.add_gem(binary)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_add_gem_rejects_invalid_payload
|
|
110
|
+
Dir.mktmpdir do |tmp|
|
|
111
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
112
|
+
|
|
113
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::InvalidGem) do
|
|
114
|
+
repo.add_gem("")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::InvalidGem) do
|
|
118
|
+
repo.add_gem("not a real gem file")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_yank_gem_moves_file_to_tomb
|
|
124
|
+
binary = File.binread(@minuscule_fixture)
|
|
125
|
+
|
|
126
|
+
Dir.mktmpdir do |tmp|
|
|
127
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
128
|
+
repo.add_gem(binary)
|
|
129
|
+
|
|
130
|
+
repo.yank_gem("minuscule_test", "0.1.0")
|
|
131
|
+
|
|
132
|
+
refute repo.gem_exists?("minuscule_test", "0.1.0")
|
|
133
|
+
assert repo.tomb_exists?("minuscule_test", "0.1.0")
|
|
134
|
+
assert_equal binary, File.binread(repo.tomb_file_path("minuscule_test", "0.1.0"))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_yank_excludes_gem_from_listings
|
|
139
|
+
binary = File.binread(@minuscule_fixture)
|
|
140
|
+
|
|
141
|
+
Dir.mktmpdir do |tmp|
|
|
142
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
143
|
+
repo.add_gem(binary)
|
|
144
|
+
repo.yank_gem("minuscule_test", "0.1.0")
|
|
145
|
+
|
|
146
|
+
assert_equal [], repo.versions_for_gem("minuscule_test")
|
|
147
|
+
assert_equal [], repo.gem_versions
|
|
148
|
+
assert_equal [], repo.compact_info("minuscule_test")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_yank_gem_raises_when_missing
|
|
153
|
+
Dir.mktmpdir do |tmp|
|
|
154
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
155
|
+
|
|
156
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::GemNotFound) do
|
|
157
|
+
repo.yank_gem("nonexistent", "1.0.0")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_yank_twice_raises_not_found
|
|
163
|
+
binary = File.binread(@minuscule_fixture)
|
|
164
|
+
|
|
165
|
+
Dir.mktmpdir do |tmp|
|
|
166
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
167
|
+
repo.add_gem(binary)
|
|
168
|
+
repo.yank_gem("minuscule_test", "0.1.0")
|
|
169
|
+
|
|
170
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::GemNotFound) do
|
|
171
|
+
repo.yank_gem("minuscule_test", "0.1.0")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_add_gem_refuses_when_tombed
|
|
177
|
+
binary = File.binread(@minuscule_fixture)
|
|
178
|
+
|
|
179
|
+
Dir.mktmpdir do |tmp|
|
|
180
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(tmp)
|
|
181
|
+
repo.add_gem(binary)
|
|
182
|
+
repo.yank_gem("minuscule_test", "0.1.0")
|
|
183
|
+
|
|
184
|
+
assert_raises(Paquette::GemServer::DirectoryGemRepository::GemYanked) do
|
|
185
|
+
repo.add_gem(binary)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_compact_info
|
|
191
|
+
info = @repository.compact_info("zip_kit")
|
|
192
|
+
assert info.is_a?(Array)
|
|
193
|
+
assert_equal 2, info.length
|
|
194
|
+
info.each do |line|
|
|
195
|
+
assert_match(/^\S+\s+\|checksum:[a-f0-9]{64},ruby:.+$/, line, "Line should match compact index format: #{line}")
|
|
196
|
+
end
|
|
197
|
+
versions = info.map { |line| line.split(" ")[0] }
|
|
198
|
+
assert_includes versions, "6.2.0"
|
|
199
|
+
assert_includes versions, "6.2.1"
|
|
200
|
+
|
|
201
|
+
info = @repository.compact_info("minuscule_test")
|
|
202
|
+
assert info.is_a?(Array)
|
|
203
|
+
assert_equal 1, info.length
|
|
204
|
+
assert_match(/^0\.1\.0\s+\|checksum:[a-f0-9]{64},ruby:.+$/, info[0])
|
|
205
|
+
|
|
206
|
+
assert_equal [], @repository.compact_info("nonexistent")
|
|
207
|
+
end
|
|
208
|
+
end
|