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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +10 -0
  3. data/README.md +90 -0
  4. data/lib/paquette/gem_server/directory_gem_repository.rb +154 -0
  5. data/lib/paquette/gem_server/gem_repacker.rb +170 -0
  6. data/lib/paquette/gem_server/gem_repository.rb +53 -0
  7. data/lib/paquette/gem_server/personalizer.rb +66 -0
  8. data/lib/paquette/gem_server/read_gated_repository.rb +74 -0
  9. data/lib/paquette/gem_server.rb +393 -0
  10. data/lib/paquette/npm_repacker.rb +153 -0
  11. data/lib/paquette/npm_server/directory_npm_repository.rb +127 -0
  12. data/lib/paquette/npm_server/gated_npm_repository.rb +66 -0
  13. data/lib/paquette/npm_server/npm_repository.rb +53 -0
  14. data/lib/paquette/npm_server.rb +98 -0
  15. data/lib/paquette/routes.rb +78 -0
  16. data/lib/paquette/subdomain_router.rb +59 -0
  17. data/lib/paquette/version.rb +3 -0
  18. data/lib/paquette.rb +12 -0
  19. data/test/fixtures/gems/minuscule_test/minuscule_test-0.1.0.gem +0 -0
  20. data/test/fixtures/gems/zip_kit/zip_kit-6.2.0.gem +0 -0
  21. data/test/fixtures/gems/zip_kit/zip_kit-6.2.1.gem +0 -0
  22. data/test/fixtures/npm/react-dropzone/react-dropzone-14.3.8.tgz +0 -0
  23. data/test/gem_server/bundler_install_test.rb +86 -0
  24. data/test/gem_server/directory_gem_repository_test.rb +208 -0
  25. data/test/gem_server/gem_repacker_test.rb +289 -0
  26. data/test/gem_server/personalizer_metadata_test.rb +49 -0
  27. data/test/gem_server/personalizer_test.rb +66 -0
  28. data/test/gem_server/read_gated_repository_test.rb +254 -0
  29. data/test/gem_server/repository_stack_test.rb +71 -0
  30. data/test/gem_server_test.rb +339 -0
  31. data/test/npm_repacker_sourcemap_test.rb +108 -0
  32. data/test/npm_repacker_test.rb +117 -0
  33. data/test/npm_server_test.rb +140 -0
  34. data/test/routes_test.rb +142 -0
  35. data/test/subdomain_router_integration_test.rb +126 -0
  36. data/test/subdomain_router_test.rb +125 -0
  37. data/test/test_helper.rb +12 -0
  38. 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
@@ -0,0 +1,3 @@
1
+ module Paquette
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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