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,71 @@
|
|
|
1
|
+
require_relative "../test_helper"
|
|
2
|
+
|
|
3
|
+
class RepositoryStackTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@gems_dir = FIXTURE_GEMS_DIR
|
|
6
|
+
@dir_repository = Paquette::GemServer::DirectoryGemRepository.new(@gems_dir)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_repository_stack_setup
|
|
10
|
+
# Test the exact stack setup as used in GemServer
|
|
11
|
+
personalized_repository = Paquette::GemServer::Personalizer.new(@dir_repository,
|
|
12
|
+
license_key: "TEST-LICENSE-123",
|
|
13
|
+
magic_comment_replacements: {"# paquette_license_info" => "TEST-LICENSE-123"})
|
|
14
|
+
gated_repository = Paquette::GemServer::ReadGatedRepository.new(personalized_repository) { |name:, version: nil| true }
|
|
15
|
+
|
|
16
|
+
# Verify the stack works end-to-end
|
|
17
|
+
assert gated_repository.gem_names.include?("minuscule_test")
|
|
18
|
+
assert gated_repository.versions_for_gem("minuscule_test").include?("0.1.0")
|
|
19
|
+
assert gated_repository.gem_exists?("minuscule_test", "0.1.0")
|
|
20
|
+
|
|
21
|
+
# Test that personalization happens through the stack
|
|
22
|
+
skip "Test gem not found" unless gated_repository.gem_exists?("minuscule_test", "0.1.0")
|
|
23
|
+
|
|
24
|
+
gem_path = gated_repository.gem_file_path("minuscule_test", "0.1.0")
|
|
25
|
+
assert File.exist?(gem_path)
|
|
26
|
+
|
|
27
|
+
# Should be a personalized gem (different from original)
|
|
28
|
+
original_path = @dir_repository.gem_file_path("minuscule_test", "0.1.0")
|
|
29
|
+
refute_equal original_path, gem_path
|
|
30
|
+
|
|
31
|
+
# Should have different checksums
|
|
32
|
+
require "digest"
|
|
33
|
+
original_checksum = Digest::SHA256.file(original_path).hexdigest
|
|
34
|
+
personalized_checksum = Digest::SHA256.file(gem_path).hexdigest
|
|
35
|
+
refute_equal original_checksum, personalized_checksum
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_stack_delegation
|
|
39
|
+
# Test that all methods are properly delegated through the stack
|
|
40
|
+
personalized_repository = Paquette::GemServer::Personalizer.new(@dir_repository,
|
|
41
|
+
license_key: "TEST-LICENSE-123",
|
|
42
|
+
magic_comment_replacements: {"# paquette_license_info" => "TEST-LICENSE-123"})
|
|
43
|
+
gated_repository = Paquette::GemServer::ReadGatedRepository.new(personalized_repository) { |name:, version: nil| true }
|
|
44
|
+
|
|
45
|
+
# Test that methods are delegated correctly
|
|
46
|
+
assert_equal @dir_repository.gem_names, gated_repository.gem_names
|
|
47
|
+
assert_equal @dir_repository.gem_versions, gated_repository.gem_versions
|
|
48
|
+
|
|
49
|
+
# Test that personalization is applied
|
|
50
|
+
skip "Test gem not found" unless gated_repository.gem_exists?("minuscule_test", "0.1.0")
|
|
51
|
+
|
|
52
|
+
# compact_info should use personalized checksums
|
|
53
|
+
personalized_info = gated_repository.compact_info("minuscule_test")
|
|
54
|
+
original_info = @dir_repository.compact_info("minuscule_test")
|
|
55
|
+
|
|
56
|
+
assert_equal original_info.length, personalized_info.length
|
|
57
|
+
|
|
58
|
+
# But checksums should be different
|
|
59
|
+
original_checksum = extract_checksum(original_info.first)
|
|
60
|
+
personalized_checksum = extract_checksum(personalized_info.first)
|
|
61
|
+
refute_equal original_checksum, personalized_checksum
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def extract_checksum(info_line)
|
|
67
|
+
if (match = info_line.match(/checksum:([a-f0-9]+)/))
|
|
68
|
+
match[1]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class GemServerTest < Minitest::Test
|
|
4
|
+
include Rack::Test::Methods
|
|
5
|
+
|
|
6
|
+
MINUSCULE_FIXTURE = File.join(FIXTURE_GEMS_DIR, "minuscule_test", "minuscule_test-0.1.0.gem")
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(FIXTURE_GEMS_DIR)
|
|
10
|
+
@app = Paquette::GemServer.new(repo)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :app
|
|
14
|
+
|
|
15
|
+
def test_root_endpoint
|
|
16
|
+
get "/"
|
|
17
|
+
assert_equal 200, last_response.status
|
|
18
|
+
assert_equal "Paquette RubyGems Repository", last_response.body
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_names_endpoint
|
|
22
|
+
get "/api/v1/names"
|
|
23
|
+
assert_equal 200, last_response.status
|
|
24
|
+
assert_equal "application/json", last_response.content_type
|
|
25
|
+
|
|
26
|
+
names = JSON.parse(last_response.body)
|
|
27
|
+
assert_equal ["minuscule_test", "zip_kit"], names.sort
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_versions_endpoint
|
|
31
|
+
get "/api/v1/versions"
|
|
32
|
+
assert_equal 200, last_response.status
|
|
33
|
+
assert_equal "application/json", last_response.content_type
|
|
34
|
+
|
|
35
|
+
versions = JSON.parse(last_response.body)
|
|
36
|
+
assert_equal 3, versions.length
|
|
37
|
+
|
|
38
|
+
zip_kit_versions = versions.select { |v| v["name"] == "zip_kit" }
|
|
39
|
+
assert_equal 2, zip_kit_versions.length
|
|
40
|
+
assert_includes zip_kit_versions.map { |v| v["number"] }, "6.2.0"
|
|
41
|
+
assert_includes zip_kit_versions.map { |v| v["number"] }, "6.2.1"
|
|
42
|
+
|
|
43
|
+
minuscule_versions = versions.select { |v| v["name"] == "minuscule_test" }
|
|
44
|
+
assert_equal 1, minuscule_versions.length
|
|
45
|
+
assert_includes minuscule_versions.map { |v| v["number"] }, "0.1.0"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_specs_endpoint
|
|
49
|
+
get "/specs.4.8"
|
|
50
|
+
assert_equal 200, last_response.status
|
|
51
|
+
assert_equal "application/octet-stream", last_response.content_type
|
|
52
|
+
|
|
53
|
+
specs = Marshal.load(last_response.body)
|
|
54
|
+
assert specs.is_a?(Array)
|
|
55
|
+
assert_equal 3, specs.length
|
|
56
|
+
|
|
57
|
+
gem_names = specs.map { |spec| spec[0] }.sort.uniq
|
|
58
|
+
assert_equal ["minuscule_test", "zip_kit"], gem_names
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_latest_specs_endpoint
|
|
62
|
+
get "/latest_specs.4.8"
|
|
63
|
+
assert_equal 200, last_response.status
|
|
64
|
+
assert_equal "application/octet-stream", last_response.content_type
|
|
65
|
+
|
|
66
|
+
specs = Marshal.load(last_response.body)
|
|
67
|
+
assert specs.is_a?(Array)
|
|
68
|
+
assert_equal 2, specs.length
|
|
69
|
+
|
|
70
|
+
gem_names = specs.map { |spec| spec[0] }.sort
|
|
71
|
+
assert_equal ["minuscule_test", "zip_kit"], gem_names
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_gem_download
|
|
75
|
+
get "/gems/zip_kit-6.2.0.gem"
|
|
76
|
+
assert_equal 200, last_response.status
|
|
77
|
+
assert_equal "application/octet-stream", last_response.content_type
|
|
78
|
+
assert last_response.body.length > 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_search_endpoint
|
|
82
|
+
get "/api/v1/search.json", query: "zip"
|
|
83
|
+
assert_equal 200, last_response.status
|
|
84
|
+
assert_equal "application/json", last_response.content_type
|
|
85
|
+
|
|
86
|
+
results = JSON.parse(last_response.body)
|
|
87
|
+
assert results.is_a?(Array)
|
|
88
|
+
assert results.any? { |r| r["name"] == "zip_kit" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Test compact index endpoints
|
|
92
|
+
def test_compact_index_names
|
|
93
|
+
get "/names"
|
|
94
|
+
assert_equal 200, last_response.status
|
|
95
|
+
assert_equal "text/plain", last_response.content_type
|
|
96
|
+
|
|
97
|
+
names = last_response.body.split("\n")
|
|
98
|
+
assert_equal ["minuscule_test", "zip_kit"], names.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_compact_index_versions
|
|
102
|
+
get "/versions"
|
|
103
|
+
assert_equal 200, last_response.status
|
|
104
|
+
assert_equal "text/plain", last_response.content_type
|
|
105
|
+
|
|
106
|
+
# Check for checksum header
|
|
107
|
+
assert last_response.headers["X-Checksum-Sha256"]
|
|
108
|
+
refute_nil last_response.headers["X-Checksum-Sha256"]
|
|
109
|
+
|
|
110
|
+
lines = last_response.body.split("\n")
|
|
111
|
+
|
|
112
|
+
# Check for timestamp header
|
|
113
|
+
assert lines[0].start_with?("created_at:")
|
|
114
|
+
assert lines[1] == "---"
|
|
115
|
+
|
|
116
|
+
# Check gem lines (skip timestamp and separator)
|
|
117
|
+
gem_lines = lines[2..]
|
|
118
|
+
assert_equal 2, gem_lines.length
|
|
119
|
+
|
|
120
|
+
# Check that each gem line has the format "gem_name versions checksum"
|
|
121
|
+
gem_lines.each do |line|
|
|
122
|
+
parts = line.split(" ")
|
|
123
|
+
assert parts.length >= 3, "Each gem line should have at least 3 parts: #{line}"
|
|
124
|
+
assert_match(/^[a-f0-9]{32}$/, parts[-1], "Checksum should be 32 hex characters: #{line}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
gem_names = gem_lines.map { |line| line.split(" ")[0] }
|
|
128
|
+
assert_equal ["minuscule_test", "zip_kit"], gem_names.sort
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_compact_index_info
|
|
132
|
+
get "/info/zip_kit"
|
|
133
|
+
assert_equal 200, last_response.status
|
|
134
|
+
assert_equal "text/plain", last_response.content_type
|
|
135
|
+
|
|
136
|
+
info_lines = last_response.body.split("\n")
|
|
137
|
+
assert_equal 2, info_lines.length
|
|
138
|
+
|
|
139
|
+
# Check that each line has the format "version |checksum:sha256_checksum,ruby:required_ruby_version"
|
|
140
|
+
info_lines.each do |line|
|
|
141
|
+
assert_match(/^\S+\s+\|checksum:[a-f0-9]{64},ruby:.+$/, line, "Line should match compact index format: #{line}")
|
|
142
|
+
|
|
143
|
+
version, rest = line.split(" ", 2)
|
|
144
|
+
assert version.match?(/^\d+\.\d+\.\d+/), "Version should be in semver format: #{version}"
|
|
145
|
+
|
|
146
|
+
assert rest.start_with?("|checksum:"), "Should have checksum prefix: #{rest}"
|
|
147
|
+
checksum_part, ruby_part = rest[1..].split(",", 2)
|
|
148
|
+
assert checksum_part.start_with?("checksum:"), "Should have checksum: #{checksum_part}"
|
|
149
|
+
checksum = checksum_part.split(":", 2)[1]
|
|
150
|
+
assert_match(/^[a-f0-9]{64}$/, checksum, "Checksum should be 64 hex characters (SHA256): #{checksum}")
|
|
151
|
+
|
|
152
|
+
assert ruby_part.start_with?("ruby:"), "Should have ruby version requirement: #{ruby_part}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
version_lines = info_lines.map { |line| line.split(" ")[0] }
|
|
156
|
+
assert_includes version_lines, "6.2.0"
|
|
157
|
+
assert_includes version_lines, "6.2.1"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_compact_index_info_nonexistent
|
|
161
|
+
get "/info/nonexistent"
|
|
162
|
+
assert_equal 404, last_response.status
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_specs_gz_endpoint
|
|
166
|
+
get "/specs.4.8.gz"
|
|
167
|
+
assert_equal 200, last_response.status
|
|
168
|
+
assert_equal "application/x-gzip", last_response.content_type
|
|
169
|
+
|
|
170
|
+
decompressed = Zlib::GzipReader.new(StringIO.new(last_response.body)).read
|
|
171
|
+
specs = Marshal.load(decompressed)
|
|
172
|
+
assert specs.is_a?(Array)
|
|
173
|
+
assert_equal 3, specs.length
|
|
174
|
+
|
|
175
|
+
gem_names = specs.map { |spec| spec[0] }.sort.uniq
|
|
176
|
+
assert_equal ["minuscule_test", "zip_kit"], gem_names
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_latest_specs_gz_endpoint
|
|
180
|
+
get "/latest_specs.4.8.gz"
|
|
181
|
+
assert_equal 200, last_response.status
|
|
182
|
+
assert_equal "application/x-gzip", last_response.content_type
|
|
183
|
+
|
|
184
|
+
decompressed = Zlib::GzipReader.new(StringIO.new(last_response.body)).read
|
|
185
|
+
specs = Marshal.load(decompressed)
|
|
186
|
+
assert specs.is_a?(Array)
|
|
187
|
+
assert_equal 2, specs.length
|
|
188
|
+
|
|
189
|
+
gem_names = specs.map { |spec| spec[0] }.sort
|
|
190
|
+
assert_equal ["minuscule_test", "zip_kit"], gem_names
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_quick_gemspec_endpoint
|
|
194
|
+
get "/quick/Marshal.4.8/zip_kit-6.2.0.gemspec.rz"
|
|
195
|
+
assert_equal 200, last_response.status
|
|
196
|
+
assert_equal "application/octet-stream", last_response.content_type
|
|
197
|
+
|
|
198
|
+
decompressed = Zlib::Inflate.inflate(last_response.body)
|
|
199
|
+
spec = Marshal.load(decompressed)
|
|
200
|
+
assert_equal "zip_kit", spec.name
|
|
201
|
+
assert_equal "6.2.0", spec.version.to_s
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_quick_gemspec_nonexistent
|
|
205
|
+
get "/quick/Marshal.4.8/nonexistent-1.0.0.gemspec.rz"
|
|
206
|
+
assert_equal 404, last_response.status
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_push_publishes_gem
|
|
210
|
+
Dir.mktmpdir do |dir|
|
|
211
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
212
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
213
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
214
|
+
|
|
215
|
+
binary = File.binread(MINUSCULE_FIXTURE)
|
|
216
|
+
|
|
217
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
218
|
+
|
|
219
|
+
assert_equal 200, session.last_response.status
|
|
220
|
+
assert_equal "text/plain", session.last_response.content_type
|
|
221
|
+
assert_equal "Successfully registered gem: minuscule_test-0.1.0", session.last_response.body
|
|
222
|
+
|
|
223
|
+
persisted = File.join(dir, "minuscule_test", "minuscule_test-0.1.0.gem")
|
|
224
|
+
assert File.exist?(persisted)
|
|
225
|
+
assert_equal binary, File.binread(persisted)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_push_rejects_duplicate
|
|
230
|
+
Dir.mktmpdir do |dir|
|
|
231
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
232
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
233
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
234
|
+
|
|
235
|
+
binary = File.binread(MINUSCULE_FIXTURE)
|
|
236
|
+
|
|
237
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
238
|
+
assert_equal 200, session.last_response.status
|
|
239
|
+
|
|
240
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
241
|
+
assert_equal 409, session.last_response.status
|
|
242
|
+
assert_match(/already exists/, session.last_response.body)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def test_push_rejects_invalid_payload
|
|
247
|
+
Dir.mktmpdir do |dir|
|
|
248
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
249
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
250
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
251
|
+
|
|
252
|
+
session.post "/api/v1/gems", "not a real gem", "CONTENT_TYPE" => "application/octet-stream"
|
|
253
|
+
assert_equal 400, session.last_response.status
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_yank_removes_gem_from_listings
|
|
258
|
+
Dir.mktmpdir do |dir|
|
|
259
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
260
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
261
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
262
|
+
|
|
263
|
+
binary = File.binread(MINUSCULE_FIXTURE)
|
|
264
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
265
|
+
|
|
266
|
+
session.delete "/api/v1/gems/yank", {gem_name: "minuscule_test", version: "0.1.0"}
|
|
267
|
+
assert_equal 200, session.last_response.status
|
|
268
|
+
assert_equal "Successfully yanked gem: minuscule_test-0.1.0", session.last_response.body
|
|
269
|
+
|
|
270
|
+
tomb = File.join(dir, "minuscule_test", "minuscule_test-0.1.0.gem.tomb")
|
|
271
|
+
assert File.exist?(tomb)
|
|
272
|
+
refute File.exist?(File.join(dir, "minuscule_test", "minuscule_test-0.1.0.gem"))
|
|
273
|
+
|
|
274
|
+
# The gem disappears from version listings and compact info
|
|
275
|
+
session.get "/api/v1/versions"
|
|
276
|
+
versions = JSON.parse(session.last_response.body)
|
|
277
|
+
assert_empty versions.select { |v| v["name"] == "minuscule_test" }
|
|
278
|
+
|
|
279
|
+
session.get "/info/minuscule_test"
|
|
280
|
+
assert_equal 404, session.last_response.status
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_yank_nonexistent_returns_404
|
|
285
|
+
Dir.mktmpdir do |dir|
|
|
286
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
287
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
288
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
289
|
+
|
|
290
|
+
session.delete "/api/v1/gems/yank", {gem_name: "nope", version: "1.0.0"}
|
|
291
|
+
assert_equal 404, session.last_response.status
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_yank_missing_params_returns_400
|
|
296
|
+
Dir.mktmpdir do |dir|
|
|
297
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
298
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
299
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
300
|
+
|
|
301
|
+
session.delete "/api/v1/gems/yank", {gem_name: "foo"}
|
|
302
|
+
assert_equal 400, session.last_response.status
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def test_push_refuses_tombed_gem
|
|
307
|
+
Dir.mktmpdir do |dir|
|
|
308
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
309
|
+
app_with_writable_dir = Paquette::GemServer.new(repo)
|
|
310
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(app_with_writable_dir))
|
|
311
|
+
|
|
312
|
+
binary = File.binread(MINUSCULE_FIXTURE)
|
|
313
|
+
|
|
314
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
315
|
+
session.delete "/api/v1/gems/yank", {gem_name: "minuscule_test", version: "0.1.0"}
|
|
316
|
+
|
|
317
|
+
session.post "/api/v1/gems", binary, "CONTENT_TYPE" => "application/octet-stream"
|
|
318
|
+
assert_equal 403, session.last_response.status
|
|
319
|
+
assert_match(/yanked/, session.last_response.body)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def test_push_and_yank_return_403_through_read_gated_repository
|
|
324
|
+
Dir.mktmpdir do |dir|
|
|
325
|
+
repo = Paquette::GemServer::DirectoryGemRepository.new(dir)
|
|
326
|
+
gated = Paquette::GemServer::ReadGatedRepository.new(repo) { |**| true }
|
|
327
|
+
read_only_app = Paquette::GemServer.new(gated)
|
|
328
|
+
session = Rack::Test::Session.new(Rack::MockSession.new(read_only_app))
|
|
329
|
+
|
|
330
|
+
session.post "/api/v1/gems", "anything", "CONTENT_TYPE" => "application/octet-stream"
|
|
331
|
+
assert_equal 403, session.last_response.status
|
|
332
|
+
assert_match(/not allowed/, session.last_response.body)
|
|
333
|
+
|
|
334
|
+
session.delete "/api/v1/gems/yank", {gem_name: "foo", version: "1.0.0"}
|
|
335
|
+
assert_equal 403, session.last_response.status
|
|
336
|
+
assert_match(/not allowed/, session.last_response.body)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class NpmRepackerSourcemapTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@temp_dir = Dir.mktmpdir("npm_repacker_sourcemap_test")
|
|
6
|
+
@package_dir = File.join(@temp_dir, "package")
|
|
7
|
+
FileUtils.mkdir_p(@package_dir)
|
|
8
|
+
|
|
9
|
+
# Create a test package with JS file and sourcemap
|
|
10
|
+
create_test_package
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
FileUtils.rm_rf(@temp_dir)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_repack_with_sourcemap_updates
|
|
18
|
+
# Create a temporary NPM package
|
|
19
|
+
package_path = create_npm_package
|
|
20
|
+
|
|
21
|
+
# Repack the package with text replacement
|
|
22
|
+
new_package_path = Paquette::NpmRepacker.repack(package_path) do |input_file, output_file, file_path|
|
|
23
|
+
unless File.extname(file_path).match?(/\.(ts|js|mjs|jsx|tsx)$/)
|
|
24
|
+
IO.copy_stream(input_file, output_file)
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
content = input_file.read
|
|
29
|
+
# Add some text to change the file length (simulating a real transformation)
|
|
30
|
+
content = content.gsub("function test()", 'function test() { console.log("modified");')
|
|
31
|
+
output_file.write(content)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
assert File.exist?(new_package_path)
|
|
35
|
+
|
|
36
|
+
# Unpack and verify the sourcemap was updated
|
|
37
|
+
verify_sourcemap_update(new_package_path)
|
|
38
|
+
|
|
39
|
+
# Clean up
|
|
40
|
+
File.delete(package_path) if File.exist?(package_path)
|
|
41
|
+
File.delete(new_package_path) if File.exist?(new_package_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def create_test_package
|
|
47
|
+
# Create a simple JS file
|
|
48
|
+
js_content = <<~JS
|
|
49
|
+
function test() {
|
|
50
|
+
return "hello world";
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=test.js.map
|
|
53
|
+
JS
|
|
54
|
+
|
|
55
|
+
# Create a simple sourcemap
|
|
56
|
+
sourcemap_content = {
|
|
57
|
+
version: 3,
|
|
58
|
+
sources: ["test.js"],
|
|
59
|
+
names: ["test"],
|
|
60
|
+
mappings: "AAAA,SAAS,SAAS,SAAS,CAAC",
|
|
61
|
+
file: "test.js"
|
|
62
|
+
}.to_json
|
|
63
|
+
|
|
64
|
+
File.write(File.join(@package_dir, "test.js"), js_content)
|
|
65
|
+
File.write(File.join(@package_dir, "test.js.map"), sourcemap_content)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def create_npm_package
|
|
69
|
+
package_path = File.join(@temp_dir, "test-package.tgz")
|
|
70
|
+
result = system("cd #{@temp_dir} && tar -czf #{package_path} package/")
|
|
71
|
+
raise "Failed to create test package" unless result
|
|
72
|
+
package_path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def verify_sourcemap_update(package_path)
|
|
76
|
+
# Unpack the repacked package
|
|
77
|
+
temp_unpack_dir = Dir.mktmpdir("verify_sourcemap")
|
|
78
|
+
begin
|
|
79
|
+
result = system("tar -xzf #{package_path} -C #{temp_unpack_dir}")
|
|
80
|
+
assert result, "Failed to unpack the repacked package"
|
|
81
|
+
|
|
82
|
+
# Find the unpacked package directory
|
|
83
|
+
package_dir = Dir.glob(File.join(temp_unpack_dir, "*")).find { |path| File.directory?(path) }
|
|
84
|
+
assert package_dir, "Could not find unpacked package directory"
|
|
85
|
+
|
|
86
|
+
# Check that the JS file was modified
|
|
87
|
+
js_file = File.join(package_dir, "test.js")
|
|
88
|
+
assert File.exist?(js_file), "JS file not found"
|
|
89
|
+
|
|
90
|
+
js_content = File.read(js_file)
|
|
91
|
+
assert js_content.include?('console.log("modified")'), "JS file was not modified"
|
|
92
|
+
|
|
93
|
+
# Check that the sourcemap was updated
|
|
94
|
+
sourcemap_file = File.join(package_dir, "test.js.map")
|
|
95
|
+
if File.exist?(sourcemap_file)
|
|
96
|
+
sourcemap_content = File.read(sourcemap_file)
|
|
97
|
+
sourcemap = JSON.parse(sourcemap_content)
|
|
98
|
+
|
|
99
|
+
# The sourcemap should have been updated (we can't easily verify the exact mapping changes
|
|
100
|
+
# without a more complex test, but we can verify it's still valid JSON)
|
|
101
|
+
assert sourcemap.is_a?(Hash), "Sourcemap is not valid JSON"
|
|
102
|
+
assert sourcemap["version"], "Sourcemap missing version"
|
|
103
|
+
end
|
|
104
|
+
ensure
|
|
105
|
+
FileUtils.rm_rf(temp_unpack_dir)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
class NpmRepackerTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@test_package_path = File.join(FIXTURE_NPM_DIR, "react-dropzone", "react-dropzone-14.3.8.tgz")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_repack_with_text_replacement
|
|
9
|
+
skip "Test package not found" unless File.exist?(@test_package_path)
|
|
10
|
+
|
|
11
|
+
# Replace the first occurrence of 'Dropzone' with 'Liftzone' in JS/TS files
|
|
12
|
+
new_package_path = Paquette::NpmRepacker.repack(@test_package_path) do |input_file, output_file, file_path|
|
|
13
|
+
unless File.extname(file_path).match?(/\.(ts|js|mjs|jsx|tsx)$/)
|
|
14
|
+
IO.copy_stream(input_file, output_file)
|
|
15
|
+
next
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Read the entire content
|
|
19
|
+
content = input_file.read
|
|
20
|
+
|
|
21
|
+
# Replace ALL occurrences of 'Dropzone' with 'Liftzone'
|
|
22
|
+
if content.include?("Dropzone")
|
|
23
|
+
content = content.gsub("Dropzone", "Liftzone")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Write the processed content
|
|
27
|
+
output_file.write(content)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
assert File.exist?(new_package_path)
|
|
31
|
+
assert new_package_path.end_with?("-repacked.tgz")
|
|
32
|
+
|
|
33
|
+
# Unpack the produced package and verify the replacement
|
|
34
|
+
verify_repacking(new_package_path)
|
|
35
|
+
|
|
36
|
+
# Clean up
|
|
37
|
+
File.delete(new_package_path) if File.exist?(new_package_path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_repack_requires_block
|
|
41
|
+
skip "Test package not found" unless File.exist?(@test_package_path)
|
|
42
|
+
|
|
43
|
+
assert_raises(ArgumentError) do
|
|
44
|
+
Paquette::NpmRepacker.repack(@test_package_path)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_repack_with_nonexistent_package
|
|
49
|
+
nonexistent_path = "/nonexistent/path.tgz"
|
|
50
|
+
|
|
51
|
+
assert_raises(ArgumentError) do
|
|
52
|
+
Paquette::NpmRepacker.repack(nonexistent_path) { |input_file, output_file, file_path| }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_repack_creates_new_package_with_different_checksum
|
|
57
|
+
skip "Test package not found" unless File.exist?(@test_package_path)
|
|
58
|
+
|
|
59
|
+
original_checksum = Digest::SHA256.file(@test_package_path).hexdigest
|
|
60
|
+
|
|
61
|
+
new_package_path = Paquette::NpmRepacker.repack(@test_package_path) do |input_file, output_file, file_path|
|
|
62
|
+
unless File.extname(file_path).match?(/\.(ts|js|mjs|jsx|tsx)$/)
|
|
63
|
+
IO.copy_stream(input_file, output_file)
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
content = input_file.read
|
|
68
|
+
content = content.gsub("Dropzone", "Liftzone") if content.include?("Dropzone")
|
|
69
|
+
output_file.write(content)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
new_checksum = Digest::SHA256.file(new_package_path).hexdigest
|
|
73
|
+
|
|
74
|
+
assert File.exist?(new_package_path)
|
|
75
|
+
refute_equal original_checksum, new_checksum
|
|
76
|
+
|
|
77
|
+
# Clean up
|
|
78
|
+
File.delete(new_package_path) if File.exist?(new_package_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def verify_repacking(package_path)
|
|
84
|
+
# Create a temporary directory to unpack the new package
|
|
85
|
+
temp_dir = Dir.mktmpdir("verify_npm_repacking")
|
|
86
|
+
unpacked_dir = File.join(temp_dir, "unpacked")
|
|
87
|
+
FileUtils.mkdir_p(unpacked_dir)
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
# Unpack the new package
|
|
91
|
+
result = system("tar -xzf #{package_path} -C #{unpacked_dir}")
|
|
92
|
+
assert result, "Failed to unpack the repacked package"
|
|
93
|
+
|
|
94
|
+
# Find the unpacked package directory
|
|
95
|
+
package_dir = Dir.glob(File.join(unpacked_dir, "*")).find { |path| File.directory?(path) }
|
|
96
|
+
assert package_dir, "Could not find unpacked package directory"
|
|
97
|
+
|
|
98
|
+
# Check JS/TS files for the replacement
|
|
99
|
+
found_replacement = false
|
|
100
|
+
extensions = %w[.ts .js .mjs .jsx .tsx]
|
|
101
|
+
extensions.each do |ext|
|
|
102
|
+
Dir.glob(File.join(package_dir, "**", "*#{ext}")).each do |js_file|
|
|
103
|
+
content = File.read(js_file)
|
|
104
|
+
if content.include?("Liftzone")
|
|
105
|
+
found_replacement = true
|
|
106
|
+
end
|
|
107
|
+
# Ensure the original word is gone (all occurrences)
|
|
108
|
+
refute content.include?("Dropzone"), "Original word 'Dropzone' still present in #{js_file}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
assert found_replacement, "Replacement word 'Liftzone' not found in any JS/TS file"
|
|
113
|
+
ensure
|
|
114
|
+
FileUtils.rm_rf(temp_dir)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|