gem_server_conformance 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/.rspec +3 -0
- data/.rubocop.yml +65 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +27 -0
- data/exe/gem_server_conformance +10 -0
- data/exe/gem_server_example +7 -0
- data/lib/gem_server_conformance/server.rb +324 -0
- data/lib/gem_server_conformance/version.rb +5 -0
- data/spec/gem_server_conformance_spec.rb +423 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/request_helpers.rb +364 -0
- data/spec/support/step_helpers.rb +257 -0
- metadata +75 -0
@@ -0,0 +1,364 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubygems/gemcutter_utilities"
|
4
|
+
|
5
|
+
module RequestHelpers
|
6
|
+
def self.included(base)
|
7
|
+
super
|
8
|
+
base.attr_reader :last_response
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_gem(name, version, platform: nil)
|
12
|
+
spec = Gem::Specification.new do |s|
|
13
|
+
s.name = name
|
14
|
+
s.version = version
|
15
|
+
s.authors = ["Conformance"]
|
16
|
+
s.summary = "Conformance test"
|
17
|
+
s.files = []
|
18
|
+
s.date = "2024-07-09"
|
19
|
+
s.platform = platform if platform
|
20
|
+
end
|
21
|
+
yield spec if block_given?
|
22
|
+
|
23
|
+
package = Gem::Package.new(StringIO.new.binmode)
|
24
|
+
package.build_time = Time.utc(1970)
|
25
|
+
package.spec = spec
|
26
|
+
package.setup_signer
|
27
|
+
signer = package.instance_variable_get(:@signer)
|
28
|
+
package.gem.with_write_io do |gem_io|
|
29
|
+
Gem::Package::TarWriter.new gem_io do |gem|
|
30
|
+
digests = gem.add_file_signed "metadata.gz", 0o444, signer do |io|
|
31
|
+
package.gzip_to io do |gz_io|
|
32
|
+
yaml = spec.to_yaml
|
33
|
+
yaml.sub!(/^rubygems_version: .*/, "rubygems_version: 3.5.11")
|
34
|
+
gz_io.write yaml
|
35
|
+
end
|
36
|
+
end
|
37
|
+
checksums = package.instance_variable_get(:@checksums)
|
38
|
+
checksums["metadata.gz"] = digests
|
39
|
+
|
40
|
+
digests = gem.add_file_signed "data.tar.gz", 0o444, signer do |io|
|
41
|
+
package.gzip_to io do |gz_io|
|
42
|
+
# no files
|
43
|
+
Gem::Package::TarWriter.new gz_io
|
44
|
+
end
|
45
|
+
end
|
46
|
+
checksums["data.tar.gz"] = digests
|
47
|
+
|
48
|
+
package.add_checksums gem
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
MockGem.new(
|
53
|
+
name: name,
|
54
|
+
version: spec.version,
|
55
|
+
platform: spec.platform,
|
56
|
+
sha256: Digest::SHA256.hexdigest(package.gem.io.string),
|
57
|
+
contents: package.gem.io.string
|
58
|
+
).tap { @all_gems << _1 }
|
59
|
+
end
|
60
|
+
|
61
|
+
MockGem = Struct.new(:name, :version, :platform, :sha256, :contents, keyword_init: true) do
|
62
|
+
def full_name
|
63
|
+
if platform == "ruby"
|
64
|
+
"#{name}-#{version}"
|
65
|
+
else
|
66
|
+
"#{name}-#{version}-#{platform}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def prerelease?
|
71
|
+
version.prerelease?
|
72
|
+
end
|
73
|
+
|
74
|
+
def size
|
75
|
+
contents.bytesize
|
76
|
+
end
|
77
|
+
|
78
|
+
def spec
|
79
|
+
io = StringIO.new(contents)
|
80
|
+
io.binmode
|
81
|
+
Gem::Package.new(io).spec
|
82
|
+
end
|
83
|
+
|
84
|
+
def pretty_print(pp)
|
85
|
+
pp.object_address_group(self) do
|
86
|
+
attr_names = %i[name version platform sha256 size]
|
87
|
+
pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
|
88
|
+
pp.breakable " "
|
89
|
+
pp.group(1) do
|
90
|
+
pp.text attr_name
|
91
|
+
pp.text ":"
|
92
|
+
pp.breakable
|
93
|
+
value = send(attr_name)
|
94
|
+
pp.pp value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module Pusher
|
102
|
+
extend Gem::GemcutterUtilities
|
103
|
+
|
104
|
+
def self.options
|
105
|
+
{}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def push_gem(gem, expected_to:)
|
110
|
+
rubygems_api_request(
|
111
|
+
:post,
|
112
|
+
"api/v1/gems",
|
113
|
+
upstream,
|
114
|
+
scope: :push_rubygem
|
115
|
+
) do |request|
|
116
|
+
request.body = gem.contents
|
117
|
+
request["Content-Length"] = gem.size.to_s
|
118
|
+
request["Content-Type"] = "application/octet-stream"
|
119
|
+
request.add_field "Authorization", Pusher.api_key
|
120
|
+
end.tap do
|
121
|
+
expect(last_response).to expected_to
|
122
|
+
set_time @time + 60
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def yank_gem(gem, expected_to:)
|
127
|
+
rubygems_api_request(
|
128
|
+
:delete,
|
129
|
+
"api/v1/gems/yank",
|
130
|
+
upstream,
|
131
|
+
scope: :yank_rubygem
|
132
|
+
) do |request|
|
133
|
+
request.body = URI.encode_www_form(
|
134
|
+
gem_name: gem.name,
|
135
|
+
version: gem.version.to_s,
|
136
|
+
platform: gem.platform
|
137
|
+
)
|
138
|
+
request["Content-Length"] = request.body.bytesize.to_s
|
139
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
140
|
+
request.add_field "Authorization", Pusher.api_key
|
141
|
+
end.tap do
|
142
|
+
expect(last_response).to expected_to, last_response.body
|
143
|
+
set_time @time + 60
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def set_time(time) # rubocop:disable Naming/AccessorMethodName
|
148
|
+
@time = time
|
149
|
+
body = time.iso8601
|
150
|
+
rubygems_api_request(
|
151
|
+
:post,
|
152
|
+
"set_time",
|
153
|
+
upstream
|
154
|
+
) do |request|
|
155
|
+
request.body = body
|
156
|
+
request["Content-Length"] = body.to_s
|
157
|
+
request["Content-Type"] = "text/plain"
|
158
|
+
end.tap do |response|
|
159
|
+
expect(response.code).to eq "200"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def rebuild_versions_list
|
164
|
+
rubygems_api_request(
|
165
|
+
:post,
|
166
|
+
"rebuild_versions_list",
|
167
|
+
upstream
|
168
|
+
) { nil }.tap do |response|
|
169
|
+
expect(response.code).to eq "200"
|
170
|
+
set_time @time + 3600
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def get_versions # rubocop:disable Naming/AccessorMethodName
|
175
|
+
rubygems_api_request(
|
176
|
+
:get,
|
177
|
+
"versions",
|
178
|
+
upstream
|
179
|
+
) { nil }
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_names # rubocop:disable Naming/AccessorMethodName
|
183
|
+
rubygems_api_request(
|
184
|
+
:get,
|
185
|
+
"names",
|
186
|
+
upstream
|
187
|
+
) { nil }
|
188
|
+
end
|
189
|
+
|
190
|
+
def get_info(name)
|
191
|
+
rubygems_api_request(
|
192
|
+
:get,
|
193
|
+
"info/#{name}",
|
194
|
+
upstream
|
195
|
+
) { nil }
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_gem(name)
|
199
|
+
rubygems_api_request(
|
200
|
+
:get,
|
201
|
+
"gems/#{name}.gem",
|
202
|
+
upstream
|
203
|
+
) { nil }
|
204
|
+
end
|
205
|
+
|
206
|
+
def get_quick_spec(name)
|
207
|
+
rubygems_api_request(
|
208
|
+
:get,
|
209
|
+
"quick/Marshal.4.8/#{name}.gemspec.rz",
|
210
|
+
upstream
|
211
|
+
) { nil }
|
212
|
+
end
|
213
|
+
|
214
|
+
def get_specs(name = nil)
|
215
|
+
path = [name, "specs.4.8.gz"].compact.join("_")
|
216
|
+
rubygems_api_request(
|
217
|
+
:get,
|
218
|
+
path,
|
219
|
+
upstream
|
220
|
+
) { nil }
|
221
|
+
end
|
222
|
+
|
223
|
+
class MockResponse
|
224
|
+
attr_reader :response
|
225
|
+
|
226
|
+
def initialize(response)
|
227
|
+
response.body_encoding = Encoding::BINARY
|
228
|
+
@response = response
|
229
|
+
end
|
230
|
+
|
231
|
+
def inspect
|
232
|
+
headers = +"HTTP/#{response.http_version} #{response.code} #{response.message}\n".b
|
233
|
+
response.each_header do |name, value|
|
234
|
+
headers << "#{name}: #{value}\n"
|
235
|
+
end
|
236
|
+
headers << "\n"
|
237
|
+
headers << response.body
|
238
|
+
end
|
239
|
+
|
240
|
+
def ok?
|
241
|
+
response.code == "200"
|
242
|
+
end
|
243
|
+
|
244
|
+
def not_found?
|
245
|
+
response.code == "404"
|
246
|
+
end
|
247
|
+
|
248
|
+
def conflict?
|
249
|
+
response.code == "409"
|
250
|
+
end
|
251
|
+
|
252
|
+
def forbidden?
|
253
|
+
response.code == "403"
|
254
|
+
end
|
255
|
+
|
256
|
+
def ==(other)
|
257
|
+
return false unless other.is_a?(self.class)
|
258
|
+
|
259
|
+
response.to_hash == other.response.to_hash &&
|
260
|
+
response.body == other.response.body
|
261
|
+
end
|
262
|
+
|
263
|
+
extend Forwardable
|
264
|
+
|
265
|
+
def_delegators :response, :code, :body, :http_version
|
266
|
+
def_delegators "response.body", :match?, :===, :=~
|
267
|
+
end
|
268
|
+
|
269
|
+
def rubygems_api_request(...)
|
270
|
+
internal = internal_request?(...)
|
271
|
+
Pusher.rubygems_api_request(...).tap do |response|
|
272
|
+
@last_response = MockResponse.new(response) unless internal
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def internal_request?(_, path, *, **)
|
277
|
+
case path
|
278
|
+
when "rebuild_versions_list", "set_time"
|
279
|
+
true
|
280
|
+
else
|
281
|
+
false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
RSpec::Matchers.define :have_header do
|
286
|
+
match do |response|
|
287
|
+
expect(response).to be_a(MockResponse)
|
288
|
+
|
289
|
+
@header_value = response.response.fetch(expected, nil)
|
290
|
+
values_match?(@value, @header_value)
|
291
|
+
end
|
292
|
+
|
293
|
+
failure_message do |_response|
|
294
|
+
super() + ", but got: #{description_of(@header_value)}"
|
295
|
+
end
|
296
|
+
|
297
|
+
chain :with_value do |value|
|
298
|
+
@value = value
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
RSpec::Matchers.define :have_body do
|
303
|
+
match do |response|
|
304
|
+
expect(response).to be_a(MockResponse)
|
305
|
+
body = response.body.b
|
306
|
+
@actual = body
|
307
|
+
values_match?(expected, body)
|
308
|
+
end
|
309
|
+
|
310
|
+
diffable
|
311
|
+
end
|
312
|
+
|
313
|
+
RSpec::Matchers.define :encoded_as do
|
314
|
+
match do |str|
|
315
|
+
@actual = str.encoding.to_s
|
316
|
+
expect(str.encoding).to eq(expected)
|
317
|
+
end
|
318
|
+
diffable
|
319
|
+
end
|
320
|
+
|
321
|
+
RSpec::Matchers.define :be_valid_compact_index_reponse do
|
322
|
+
match notify_expectation_failures: true do |response|
|
323
|
+
expect(response).to be_a(MockResponse)
|
324
|
+
.and be_ok
|
325
|
+
.and have_header("content-type").with_value("text/plain; charset=utf-8")
|
326
|
+
.and have_header("accept-ranges").with_value("bytes")
|
327
|
+
.and have_header("digest").with_value("sha-256=#{Digest::SHA256.base64digest(response.body)}")
|
328
|
+
.and have_header("repr-digest").with_value("sha-256=:#{Digest::SHA256.base64digest(response.body)}:")
|
329
|
+
.and have_header("etag").with_value("\"#{Digest::MD5.hexdigest(response.body)}\"")
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
RSpec::Matchers.define :have_content_length do
|
334
|
+
match do |response|
|
335
|
+
expect(response).to be_a(MockResponse)
|
336
|
+
expect(response).to have_header("content-length").with_value(response.body.bytesize.to_s)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
RSpec::Matchers.define :be_unchanged do
|
341
|
+
match do |response|
|
342
|
+
expect(response).to be_a(MockResponse)
|
343
|
+
expect(response).to be_not_found
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
RSpec::Matchers.define :unmarshal_as do
|
348
|
+
match do |response|
|
349
|
+
expect(response).to be_a(MockResponse)
|
350
|
+
body = response.body.b
|
351
|
+
body = if must_inflate
|
352
|
+
Zlib.inflate(body)
|
353
|
+
else
|
354
|
+
Zlib.gunzip(body)
|
355
|
+
end
|
356
|
+
@actual = Marshal.load(body)
|
357
|
+
values_match?(expected, @actual)
|
358
|
+
end
|
359
|
+
|
360
|
+
chain :inflate, :must_inflate
|
361
|
+
|
362
|
+
diffable
|
363
|
+
end
|
364
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StepHelpers
|
4
|
+
class Step
|
5
|
+
attr_reader :parent, :requests, :children, :last_responses, :blk
|
6
|
+
|
7
|
+
def initialize(ctx, parent = nil, &blk)
|
8
|
+
@ctx = ctx
|
9
|
+
@parent = parent
|
10
|
+
@requests = []
|
11
|
+
@last_responses = {}
|
12
|
+
@children = []
|
13
|
+
@parent&.children&.<< self
|
14
|
+
@blk = blk
|
15
|
+
define! unless parent
|
16
|
+
end
|
17
|
+
|
18
|
+
def previous_requests
|
19
|
+
return [] unless @parent
|
20
|
+
|
21
|
+
req = @parent.requests + @parent.previous_requests
|
22
|
+
req.uniq!
|
23
|
+
|
24
|
+
req.reject! do |m, a, _k, _|
|
25
|
+
requests.any? { |m_2, a_2, _, _| m == m_2 && a == a_2 }
|
26
|
+
end
|
27
|
+
req
|
28
|
+
end
|
29
|
+
|
30
|
+
def then(message, before: nil, **kwargs, &blk)
|
31
|
+
raise ArgumentError, "block required" unless blk
|
32
|
+
raise ArgumentError, "message required" unless message
|
33
|
+
raise "already has children" if @children.any?
|
34
|
+
|
35
|
+
Step.new(@ctx.instance_eval { context(message, **kwargs) }, self).tap do |step|
|
36
|
+
step.instance_variable_get(:@ctx).before(:all, &before) if before
|
37
|
+
step.instance_eval(&blk)
|
38
|
+
step.define!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# class NullReporter
|
43
|
+
# def self.method_missing(...)
|
44
|
+
# pp(...)
|
45
|
+
# end
|
46
|
+
|
47
|
+
# def self.example_failed(ex)
|
48
|
+
# pp ex
|
49
|
+
# puts ex.display_exception.full_message
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
|
53
|
+
def request(method, *args, **kwargs, &blk)
|
54
|
+
name = method.to_s
|
55
|
+
name += "(#{args.map(&:inspect).join(", ")})" unless args.empty?
|
56
|
+
name += " unchanged" if kwargs[:unchanged]
|
57
|
+
step = self
|
58
|
+
|
59
|
+
reset_examples = proc do |ctx|
|
60
|
+
ctx.examples.each do |example|
|
61
|
+
example.display_exception = nil
|
62
|
+
example.metadata[:execution_result] = RSpec::Core::Example::ExecutionResult.new
|
63
|
+
end
|
64
|
+
ctx.children.each(&reset_examples)
|
65
|
+
end
|
66
|
+
|
67
|
+
@ctx.context "#{name} response", **kwargs do
|
68
|
+
before(:all) do
|
69
|
+
20.times do
|
70
|
+
send(method, *args)
|
71
|
+
step.last_responses[[method, *args]] = last_response
|
72
|
+
|
73
|
+
reporter = RSpec::Core::NullReporter
|
74
|
+
self.class.store_before_context_ivars(self)
|
75
|
+
result_for_this_group = self.class.run_examples(reporter)
|
76
|
+
results_for_descendants = self.class.ordering_strategy.order(self.class.children).map do |child|
|
77
|
+
child.run(reporter)
|
78
|
+
end.all?
|
79
|
+
|
80
|
+
reset_examples[self.class]
|
81
|
+
break if result_for_this_group && results_for_descendants
|
82
|
+
|
83
|
+
sleep 0.1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
let(:parent_response) do
|
88
|
+
step.parent.last_responses[[method, *args]]
|
89
|
+
end
|
90
|
+
|
91
|
+
alias_method :subject, :last_response
|
92
|
+
instance_eval(&blk)
|
93
|
+
end
|
94
|
+
|
95
|
+
@requests << [method, args, kwargs, blk, self]
|
96
|
+
end
|
97
|
+
|
98
|
+
def pushed_gem(full_name)
|
99
|
+
request(:get_gem, full_name) do
|
100
|
+
let(:gem) { @all_gems.reverse_each.find { _1.full_name == full_name } || raise("gem not found") }
|
101
|
+
it { is_expected.to be_ok }
|
102
|
+
|
103
|
+
it { is_expected.to have_header("content-type").with_value("application/octet-stream") }
|
104
|
+
.metadata[:content_type_header] = true
|
105
|
+
it { is_expected.to have_content_length }
|
106
|
+
.metadata[:content_length_header] = true
|
107
|
+
it { is_expected.to have_body(eq(gem.contents)) }
|
108
|
+
end
|
109
|
+
|
110
|
+
request(:get_quick_spec, full_name) do
|
111
|
+
let(:gem) { @all_gems.reverse_each.find { _1.full_name == full_name } || raise("gem not found") }
|
112
|
+
|
113
|
+
it { is_expected.to be_ok }
|
114
|
+
|
115
|
+
it { is_expected.to have_header("content-type").with_value("application/octet-stream") }
|
116
|
+
.metadata[:content_type_header] = true
|
117
|
+
it { is_expected.to have_content_length }
|
118
|
+
.metadata[:content_length_header] = true
|
119
|
+
it { is_expected.to unmarshal_as(gem.spec.tap(&:sanitize).tap(&:abbreviate)).inflate(true) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def yanked_gem(full_name)
|
124
|
+
request(:get_gem, full_name) do
|
125
|
+
let(:gem) { @all_gems.reverse_each.find { _1.full_name == full_name } || raise("gem not found") }
|
126
|
+
|
127
|
+
it { is_expected.to be_not_found.or be_forbidden }
|
128
|
+
it { is_expected.not_to have_body(including(gem.contents)) }
|
129
|
+
end
|
130
|
+
|
131
|
+
request(:get_quick_spec, full_name) do
|
132
|
+
let(:gem) { @all_gems.reverse_each.find { _1.full_name == full_name } || raise("gem not found") }
|
133
|
+
|
134
|
+
it { is_expected.to be_not_found.or be_forbidden }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def define!
|
139
|
+
step = self
|
140
|
+
|
141
|
+
instance_eval(&step.blk) if step.blk
|
142
|
+
|
143
|
+
if step.parent.nil?
|
144
|
+
@ctx.instance_eval do
|
145
|
+
attr_reader :upstream
|
146
|
+
|
147
|
+
before(:all) do
|
148
|
+
@upstream = ENV.fetch("UPSTREAM", nil)
|
149
|
+
unless upstream
|
150
|
+
Bundler.with_original_env do
|
151
|
+
@upstream = "http://localhost:4567"
|
152
|
+
@pid = spawn("ruby", "-rbundler/setup", "lib/gem_server_conformance/server.rb", out: "/dev/null",
|
153
|
+
err: "/dev/null")
|
154
|
+
sleep 1
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
@all_gems = []
|
159
|
+
set_time Time.utc(1990)
|
160
|
+
end
|
161
|
+
|
162
|
+
after(:all) do
|
163
|
+
if @pid
|
164
|
+
Process.kill "TERM", @pid
|
165
|
+
Process.wait @pid
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
step.previous_requests.each do |method, args, kwargs, blk, s|
|
172
|
+
request(method, *args, unchanged: true, **kwargs) do
|
173
|
+
let(:parent_response) do
|
174
|
+
s.parent.last_responses[[method, *args]]
|
175
|
+
end
|
176
|
+
instance_eval(&blk)
|
177
|
+
if kwargs[:compact_index]
|
178
|
+
it "is expected to have the same etag" do
|
179
|
+
is_expected.to have_header("ETag").with_value(step.parent.last_responses[[method,
|
180
|
+
*args]].response["ETag"])
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
if requests.any?
|
187
|
+
@ctx.context "/versions", compact_index: true do
|
188
|
+
it "has matching etags" do
|
189
|
+
expect(step.last_responses).to include([:get_versions])
|
190
|
+
versions_response = step.last_responses[[:get_versions]]
|
191
|
+
|
192
|
+
expected = versions_response.body.lines.to_h do |l|
|
193
|
+
n, _, e = l.split
|
194
|
+
[n, "\"#{e}\""]
|
195
|
+
end
|
196
|
+
expected.delete("---")
|
197
|
+
expected.delete("created_at:")
|
198
|
+
|
199
|
+
etags = step.last_responses.filter_map do |k, v|
|
200
|
+
next unless k.first == :get_info
|
201
|
+
next unless v.ok?
|
202
|
+
|
203
|
+
[k[1], v.response["ETag"]]
|
204
|
+
end.to_h
|
205
|
+
|
206
|
+
expect(etags).to eq(expected)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
@ctx.context "all expected requests", compact_index: true do
|
211
|
+
it "are tested" do
|
212
|
+
expect(step.last_responses).to include([:get_versions])
|
213
|
+
versions_response = step.last_responses[[:get_versions]]
|
214
|
+
|
215
|
+
expected = versions_response.body.lines.flat_map do |l|
|
216
|
+
next if l.start_with?("---")
|
217
|
+
next if l.start_with?("created_at:")
|
218
|
+
|
219
|
+
n, versions, = l.split
|
220
|
+
versions.split(",").flat_map do |v|
|
221
|
+
v.delete_prefix!("-")
|
222
|
+
[
|
223
|
+
[:get_quick_spec, ["#{n}-#{v}"]],
|
224
|
+
[:get_gem, ["#{n}-#{v}"]]
|
225
|
+
]
|
226
|
+
end << [:get_info, [n]]
|
227
|
+
end
|
228
|
+
expected.compact!
|
229
|
+
expected.uniq!
|
230
|
+
|
231
|
+
actual = step.requests.map { |m, a, _| [m, a] }
|
232
|
+
|
233
|
+
missing = expected - actual
|
234
|
+
|
235
|
+
expect(missing).to be_empty
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
children.each(&:define!)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
module ClassMethods
|
245
|
+
def all_requests
|
246
|
+
@all_requests ||= []
|
247
|
+
end
|
248
|
+
|
249
|
+
def all_requests_indices
|
250
|
+
@all_requests_indices ||= {}
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.included(base)
|
255
|
+
base.extend(ClassMethods)
|
256
|
+
end
|
257
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gem_server_conformance
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samuel Giddins
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- segiddins@rubycentral.org
|
30
|
+
executables:
|
31
|
+
- gem_server_conformance
|
32
|
+
- gem_server_example
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".rspec"
|
37
|
+
- ".rubocop.yml"
|
38
|
+
- ".ruby-version"
|
39
|
+
- CHANGELOG.md
|
40
|
+
- LICENSE.txt
|
41
|
+
- README.md
|
42
|
+
- exe/gem_server_conformance
|
43
|
+
- exe/gem_server_example
|
44
|
+
- lib/gem_server_conformance/server.rb
|
45
|
+
- lib/gem_server_conformance/version.rb
|
46
|
+
- spec/gem_server_conformance_spec.rb
|
47
|
+
- spec/spec_helper.rb
|
48
|
+
- spec/support/request_helpers.rb
|
49
|
+
- spec/support/step_helpers.rb
|
50
|
+
homepage: https://github.com/rubygems/gem_server_conformance
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata:
|
54
|
+
homepage_uri: https://github.com/rubygems/gem_server_conformance
|
55
|
+
rubygems_mfa_required: 'true'
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 3.0.0
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubygems_version: 3.5.11
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: A conformance test suite for RubyGems servers.
|
75
|
+
test_files: []
|