shiny-sdk 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6e8b4f3ec25fb2c30b0dcc9b8255d1c5d3d372b02cfe882deac9b376d4d4281
4
- data.tar.gz: 28aa69a85ef6a4dd6cd3f98a8ab8f7fdac212ba67c5f265488927d430b31675e
3
+ metadata.gz: 8bbc4ff03c35b19c7ea076e103315600e43f7c2888be0f3d4c921ef60ebfa588
4
+ data.tar.gz: 7b375a311cbcaadaaef8eee4490e53fc41f7a5eb1e5e71e53e7003b155beb166
5
5
  SHA512:
6
- metadata.gz: 3b4d3eb499dea9119c4a5792063247bd2e2e70629bbc8c526d1ae9bfcfc9d00d2c3f14b8c653cd3bdbd6c31a6b9bc685bb2dcc425adaa0ce081290b95fa44a37
7
- data.tar.gz: 27773500cbe7fb9aeceaf6c8c3650e385c1317366e2c7da111a73af4a28854b78a828865d3f77f29596c0efb8e5fdb071a246572b3e1f8f29d7e13b7b1df814d
6
+ metadata.gz: 5c17b84dbc872a8e32490b97a593e44a28a1494e14accf1ba672871b8c27b4696bf1f6943330c44a3c7b453a9b8bd19e469a6ff269d1be03f2725809e7db923e
7
+ data.tar.gz: 571e95ad7ebb2835f2e5cb43ccd27909d542786b5162b465bae528745b9d548d5cdc9e0dcb58d4a9f1534581688dbb6ec431bb5c20656a72575a2cb36edf6f35
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ name: Ruby ${{ matrix.ruby }}
18
+ runs-on: ubuntu-22.04
19
+ timeout-minutes: 10
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
24
+ steps:
25
+ - name: Check out source
26
+ uses: actions/checkout@v6
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - name: Set up Ruby and install dependencies
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: ${{ matrix.ruby }}
34
+ bundler-cache: true
35
+
36
+ - name: Run tests
37
+ run: bundle exec ruby -Itest test/shiny_sdk_test.rb
38
+
39
+ - name: Build gem
40
+ run: bundle exec gem build shiny-sdk.gemspec
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /Gemfile.lock
2
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE CHANGED
File without changes
data/README.md CHANGED
@@ -20,3 +20,12 @@ shiny.add(spider_name:String, level:Integer, data:Hash, hash:String)
20
20
  # 其中前三项为必填,hash为选填。
21
21
  # 不填写hash 默认把data MD5 一次做hash。
22
22
  ```
23
+
24
+ ## 测试
25
+
26
+ ```bash
27
+ bundle install
28
+ bundle exec ruby -Itest test/shiny_sdk_test.rb
29
+ ```
30
+
31
+ 测试使用本地模拟请求,不连接真实服务。`test/fixtures/add_compatibility.json` 保存了修复前 `add()` 的 17 组行为基准,用于逐字节比对请求 JSON、签名和表单编码,并检查返回值及异常类型;`ShinyError` 的错误信息也会比对。
data/lib/shiny-sdk.rb CHANGED
@@ -24,7 +24,7 @@ class Shiny
24
24
  Hash[payload.sort].keys.each do |key|
25
25
  data += payload[key].to_s
26
26
  end
27
- data
27
+ sha1(data)
28
28
  end
29
29
 
30
30
 
@@ -86,10 +86,10 @@ class Shiny
86
86
  # 添加多个事件
87
87
  def add_many(events)
88
88
  payload = {}
89
- url = @API_HOST + "Data/add"
89
+ url = @API_HOST + "/Data/add"
90
90
  events.each do |event|
91
91
  if event[:hash].nil?
92
- if event[:data].nil?
92
+ unless event[:data].nil?
93
93
  if event[:data][:hash]
94
94
  event[:hash] = event[:data][:hash]
95
95
  event[:data].delete(:hash)
@@ -99,8 +99,8 @@ class Shiny
99
99
  else
100
100
  raise ShinyError.new("Missing parameters.")
101
101
  end
102
- event[:level] = event[:level].to_i
103
102
  end
103
+ event[:level] = event[:level].to_i
104
104
  end
105
105
  payload[:event] = events.to_json
106
106
 
@@ -140,7 +140,11 @@ class Shiny
140
140
  end
141
141
 
142
142
  def get_jobs
143
- url = @API_HOST + "/Job/query?api_key=#{@API_KEY}&sign=#{sign}"
143
+ query = URI.encode_www_form(api_key: @API_KEY, sign: sign)
144
+ url = @API_HOST + "/Job/query?#{query}"
145
+ uri = URI.parse(url)
146
+ http = Net::HTTP.new(uri.host, uri.port)
147
+ http.use_ssl = true if uri.scheme == 'https'
144
148
  response = http.start {|http|
145
149
  http.request Net::HTTP::Get.new(uri.request_uri)
146
150
  }
@@ -152,12 +156,13 @@ class Shiny
152
156
  end
153
157
 
154
158
  def report(job_id, status)
155
- url = @API_HOST + "Job/report"
159
+ url = @API_HOST + "/Job/report"
156
160
  payload = {
157
161
  "jobId": job_id,
158
162
  "status": status
159
163
  }
160
- payload_sign = sign(payload)
164
+ payload[:sign] = sign(payload)
165
+ payload[:api_key] = @API_KEY
161
166
  response = Net::HTTP.post_form(URI.parse(url), payload)
162
167
  if response.code != '200'
163
168
  begin
@@ -171,4 +176,4 @@ class Shiny
171
176
  JSON.parse(response.body)
172
177
  end
173
178
  end
174
- end
179
+ end
data/shiny-sdk.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'shiny-sdk'
3
- s.version = '1.0.0'
3
+ s.version = '1.0.2'
4
4
  s.date = `git log --pretty="%ai" -n 1`.split(" ").first
5
5
  s.summary = "Shiny SDK"
6
6
  s.description = "Shiny Ruby SDK"
@@ -13,4 +13,5 @@ Gem::Specification.new do |s|
13
13
  }
14
14
  s.license = 'MIT'
15
15
  s.required_ruby_version = '>= 2.6.0'
16
- end
16
+ s.add_development_dependency 'minitest', '~> 5.0'
17
+ end
@@ -0,0 +1,595 @@
1
+ [
2
+ {
3
+ "name": "omitted_data",
4
+ "args": [
5
+ "test",
6
+ 1
7
+ ],
8
+ "client_args": [
9
+ "test-api-key",
10
+ "test-secret",
11
+ "https://example.invalid"
12
+ ],
13
+ "response": [
14
+ "200",
15
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
16
+ ],
17
+ "request": {
18
+ "url": "https://example.invalid/Data/add",
19
+ "payload": {
20
+ "api_key": "test-api-key",
21
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
22
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
23
+ },
24
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
25
+ },
26
+ "result": {
27
+ "ok": true,
28
+ "id": 17,
29
+ "nested": {
30
+ "a": [
31
+ 1,
32
+ null
33
+ ]
34
+ }
35
+ }
36
+ },
37
+ {
38
+ "name": "nil_data",
39
+ "args": [
40
+ "test",
41
+ "2",
42
+ null
43
+ ],
44
+ "client_args": [
45
+ "test-api-key",
46
+ "test-secret",
47
+ "https://example.invalid"
48
+ ],
49
+ "response": [
50
+ "200",
51
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
52
+ ],
53
+ "request": {
54
+ "url": "https://example.invalid/Data/add",
55
+ "payload": {
56
+ "api_key": "test-api-key",
57
+ "sign": "e6faba95cb4aad55ab58ac870dc8856cc2160683",
58
+ "event": "{\"level\":2,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
59
+ },
60
+ "body": "api_key=test-api-key&sign=e6faba95cb4aad55ab58ac870dc8856cc2160683&event=%7B%22level%22%3A2%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
61
+ },
62
+ "result": {
63
+ "ok": true,
64
+ "id": 17,
65
+ "nested": {
66
+ "a": [
67
+ 1,
68
+ null
69
+ ]
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "name": "nested_unicode_data",
75
+ "args": [
76
+ "爬虫",
77
+ "3",
78
+ {
79
+ "z": [
80
+ true,
81
+ false,
82
+ null,
83
+ 1.5
84
+ ],
85
+ "a": {
86
+ "消息": "你好 &+=?/ 😀"
87
+ }
88
+ }
89
+ ],
90
+ "client_args": [
91
+ "test-api-key",
92
+ "test-secret",
93
+ "https://example.invalid"
94
+ ],
95
+ "response": [
96
+ "200",
97
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
98
+ ],
99
+ "request": {
100
+ "url": "https://example.invalid/Data/add",
101
+ "payload": {
102
+ "api_key": "test-api-key",
103
+ "sign": "4922c9cc8b952dc02c3f749153552242a505c25e",
104
+ "event": "{\"level\":3,\"spiderName\":\"爬虫\",\"hash\":\"950f474e0aadaabb0c78f543c059c28b\",\"data\":{\"z\":[true,false,null,1.5],\"a\":{\"消息\":\"你好 &+=?/ 😀\"}}}"
105
+ },
106
+ "body": "api_key=test-api-key&sign=4922c9cc8b952dc02c3f749153552242a505c25e&event=%7B%22level%22%3A3%2C%22spiderName%22%3A%22%E7%88%AC%E8%99%AB%22%2C%22hash%22%3A%22950f474e0aadaabb0c78f543c059c28b%22%2C%22data%22%3A%7B%22z%22%3A%5Btrue%2Cfalse%2Cnull%2C1.5%5D%2C%22a%22%3A%7B%22%E6%B6%88%E6%81%AF%22%3A%22%E4%BD%A0%E5%A5%BD+%26%2B%3D%3F%2F+%F0%9F%98%80%22%7D%7D%7D"
107
+ },
108
+ "result": {
109
+ "ok": true,
110
+ "id": 17,
111
+ "nested": {
112
+ "a": [
113
+ 1,
114
+ null
115
+ ]
116
+ }
117
+ }
118
+ },
119
+ {
120
+ "name": "manual_hash",
121
+ "args": [
122
+ "test",
123
+ 1,
124
+ {
125
+ "b": 2,
126
+ "a": 1
127
+ },
128
+ "fixed-hash"
129
+ ],
130
+ "client_args": [
131
+ "test-api-key",
132
+ "test-secret",
133
+ "https://example.invalid"
134
+ ],
135
+ "response": [
136
+ "200",
137
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
138
+ ],
139
+ "request": {
140
+ "url": "https://example.invalid/Data/add",
141
+ "payload": {
142
+ "api_key": "test-api-key",
143
+ "sign": "acf3c5cbfef78ab9c7ef0c5502ebedbb0d4aa571",
144
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"fixed-hash\",\"data\":{\"b\":2,\"a\":1}}"
145
+ },
146
+ "body": "api_key=test-api-key&sign=acf3c5cbfef78ab9c7ef0c5502ebedbb0d4aa571&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%22fixed-hash%22%2C%22data%22%3A%7B%22b%22%3A2%2C%22a%22%3A1%7D%7D"
147
+ },
148
+ "result": {
149
+ "ok": true,
150
+ "id": 17,
151
+ "nested": {
152
+ "a": [
153
+ 1,
154
+ null
155
+ ]
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "name": "false_hash",
161
+ "args": [
162
+ "test",
163
+ 1,
164
+ {
165
+ "b": 2,
166
+ "a": 1
167
+ },
168
+ false
169
+ ],
170
+ "client_args": [
171
+ "test-api-key",
172
+ "test-secret",
173
+ "https://example.invalid"
174
+ ],
175
+ "response": [
176
+ "200",
177
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
178
+ ],
179
+ "request": {
180
+ "url": "https://example.invalid/Data/add",
181
+ "payload": {
182
+ "api_key": "test-api-key",
183
+ "sign": "26753a8a893344f20135bcfbf667d2ff736fe48c",
184
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"9915965eb40d343a8fe26e4e341d1a05\",\"data\":{\"b\":2,\"a\":1}}"
185
+ },
186
+ "body": "api_key=test-api-key&sign=26753a8a893344f20135bcfbf667d2ff736fe48c&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%229915965eb40d343a8fe26e4e341d1a05%22%2C%22data%22%3A%7B%22b%22%3A2%2C%22a%22%3A1%7D%7D"
187
+ },
188
+ "result": {
189
+ "ok": true,
190
+ "id": 17,
191
+ "nested": {
192
+ "a": [
193
+ 1,
194
+ null
195
+ ]
196
+ }
197
+ }
198
+ },
199
+ {
200
+ "name": "empty_hash",
201
+ "args": [
202
+ "test",
203
+ 1,
204
+ {},
205
+ ""
206
+ ],
207
+ "client_args": [
208
+ "test-api-key",
209
+ "test-secret",
210
+ "https://example.invalid"
211
+ ],
212
+ "response": [
213
+ "200",
214
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
215
+ ],
216
+ "request": {
217
+ "url": "https://example.invalid/Data/add",
218
+ "payload": {
219
+ "api_key": "test-api-key",
220
+ "sign": "06f9e44e0dade5ee2a0db6c1445b91734baac875",
221
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"\",\"data\":{}}"
222
+ },
223
+ "body": "api_key=test-api-key&sign=06f9e44e0dade5ee2a0db6c1445b91734baac875&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%22%22%2C%22data%22%3A%7B%7D%7D"
224
+ },
225
+ "result": {
226
+ "ok": true,
227
+ "id": 17,
228
+ "nested": {
229
+ "a": [
230
+ 1,
231
+ null
232
+ ]
233
+ }
234
+ }
235
+ },
236
+ {
237
+ "name": "channel",
238
+ "args": [
239
+ "test",
240
+ 1,
241
+ {}
242
+ ],
243
+ "channel": "邮件 & notifications",
244
+ "client_args": [
245
+ "test-api-key",
246
+ "test-secret",
247
+ "https://example.invalid"
248
+ ],
249
+ "response": [
250
+ "200",
251
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
252
+ ],
253
+ "request": {
254
+ "url": "https://example.invalid/Data/add",
255
+ "payload": {
256
+ "api_key": "test-api-key",
257
+ "sign": "fd99bdb351bdccc8eda2f8488b251b342799411b",
258
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{},\"channel\":\"邮件 & notifications\"}"
259
+ },
260
+ "body": "api_key=test-api-key&sign=fd99bdb351bdccc8eda2f8488b251b342799411b&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%2C%22channel%22%3A%22%E9%82%AE%E4%BB%B6+%26+notifications%22%7D"
261
+ },
262
+ "result": {
263
+ "ok": true,
264
+ "id": 17,
265
+ "nested": {
266
+ "a": [
267
+ 1,
268
+ null
269
+ ]
270
+ }
271
+ }
272
+ },
273
+ {
274
+ "name": "false_channel",
275
+ "args": [
276
+ "test",
277
+ 1,
278
+ {}
279
+ ],
280
+ "channel": false,
281
+ "client_args": [
282
+ "test-api-key",
283
+ "test-secret",
284
+ "https://example.invalid"
285
+ ],
286
+ "response": [
287
+ "200",
288
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
289
+ ],
290
+ "request": {
291
+ "url": "https://example.invalid/Data/add",
292
+ "payload": {
293
+ "api_key": "test-api-key",
294
+ "sign": "170bee1d209d8a376fe28bb11887220cca2bb8f4",
295
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{},\"channel\":false}"
296
+ },
297
+ "body": "api_key=test-api-key&sign=170bee1d209d8a376fe28bb11887220cca2bb8f4&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%2C%22channel%22%3Afalse%7D"
298
+ },
299
+ "result": {
300
+ "ok": true,
301
+ "id": 17,
302
+ "nested": {
303
+ "a": [
304
+ 1,
305
+ null
306
+ ]
307
+ }
308
+ }
309
+ },
310
+ {
311
+ "name": "nil_channel",
312
+ "args": [
313
+ "test",
314
+ 1,
315
+ {}
316
+ ],
317
+ "channel": null,
318
+ "client_args": [
319
+ "test-api-key",
320
+ "test-secret",
321
+ "https://example.invalid"
322
+ ],
323
+ "response": [
324
+ "200",
325
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
326
+ ],
327
+ "request": {
328
+ "url": "https://example.invalid/Data/add",
329
+ "payload": {
330
+ "api_key": "test-api-key",
331
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
332
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
333
+ },
334
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
335
+ },
336
+ "result": {
337
+ "ok": true,
338
+ "id": 17,
339
+ "nested": {
340
+ "a": [
341
+ 1,
342
+ null
343
+ ]
344
+ }
345
+ }
346
+ },
347
+ {
348
+ "name": "invalid_level",
349
+ "args": [
350
+ "test",
351
+ "invalid",
352
+ {}
353
+ ],
354
+ "client_args": [
355
+ "test-api-key",
356
+ "test-secret",
357
+ "https://example.invalid"
358
+ ],
359
+ "response": [
360
+ "200",
361
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
362
+ ],
363
+ "request": {
364
+ "url": "https://example.invalid/Data/add",
365
+ "payload": {
366
+ "api_key": "test-api-key",
367
+ "sign": "269656dbe7c64f9cce5c56749a26bf7aa08a4abf",
368
+ "event": "{\"level\":0,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
369
+ },
370
+ "body": "api_key=test-api-key&sign=269656dbe7c64f9cce5c56749a26bf7aa08a4abf&event=%7B%22level%22%3A0%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
371
+ },
372
+ "result": {
373
+ "ok": true,
374
+ "id": 17,
375
+ "nested": {
376
+ "a": [
377
+ 1,
378
+ null
379
+ ]
380
+ }
381
+ }
382
+ },
383
+ {
384
+ "name": "string_data",
385
+ "args": [
386
+ "test",
387
+ 1,
388
+ "plain text"
389
+ ],
390
+ "client_args": [
391
+ "test-api-key",
392
+ "test-secret",
393
+ "https://example.invalid"
394
+ ],
395
+ "response": [
396
+ "200",
397
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
398
+ ],
399
+ "request": {
400
+ "url": "https://example.invalid/Data/add",
401
+ "payload": {
402
+ "api_key": "test-api-key",
403
+ "sign": "6a1df1f8cf968cdb4bebc52f307462338f188dd7",
404
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"a242b30718ba7d56ae982c65c6383976\",\"data\":\"plain text\"}"
405
+ },
406
+ "body": "api_key=test-api-key&sign=6a1df1f8cf968cdb4bebc52f307462338f188dd7&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%22a242b30718ba7d56ae982c65c6383976%22%2C%22data%22%3A%22plain+text%22%7D"
407
+ },
408
+ "result": {
409
+ "ok": true,
410
+ "id": 17,
411
+ "nested": {
412
+ "a": [
413
+ 1,
414
+ null
415
+ ]
416
+ }
417
+ }
418
+ },
419
+ {
420
+ "name": "default_host",
421
+ "client_args": [
422
+ "test-api-key",
423
+ "test-secret"
424
+ ],
425
+ "args": [
426
+ "test",
427
+ 1,
428
+ {}
429
+ ],
430
+ "response": [
431
+ "200",
432
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
433
+ ],
434
+ "request": {
435
+ "url": "https://shiny.kotori.moe/Data/add",
436
+ "payload": {
437
+ "api_key": "test-api-key",
438
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
439
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
440
+ },
441
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
442
+ },
443
+ "result": {
444
+ "ok": true,
445
+ "id": 17,
446
+ "nested": {
447
+ "a": [
448
+ 1,
449
+ null
450
+ ]
451
+ }
452
+ }
453
+ },
454
+ {
455
+ "name": "api_error",
456
+ "args": [
457
+ "test",
458
+ 1,
459
+ {}
460
+ ],
461
+ "response": [
462
+ "403",
463
+ "{\"error\":{\"info\":\"invalid signature\",\"code\":401}}"
464
+ ],
465
+ "client_args": [
466
+ "test-api-key",
467
+ "test-secret",
468
+ "https://example.invalid"
469
+ ],
470
+ "request": {
471
+ "url": "https://example.invalid/Data/add",
472
+ "payload": {
473
+ "api_key": "test-api-key",
474
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
475
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
476
+ },
477
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
478
+ },
479
+ "error": {
480
+ "class": "ShinyError",
481
+ "message": "Shiny error: invalid signature code=401"
482
+ }
483
+ },
484
+ {
485
+ "name": "non_json_http_error",
486
+ "args": [
487
+ "test",
488
+ 1,
489
+ {}
490
+ ],
491
+ "response": [
492
+ "502",
493
+ "Bad Gateway"
494
+ ],
495
+ "client_args": [
496
+ "test-api-key",
497
+ "test-secret",
498
+ "https://example.invalid"
499
+ ],
500
+ "request": {
501
+ "url": "https://example.invalid/Data/add",
502
+ "payload": {
503
+ "api_key": "test-api-key",
504
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
505
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
506
+ },
507
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
508
+ },
509
+ "error": {
510
+ "class": "ShinyError",
511
+ "message": "Network error: 502"
512
+ }
513
+ },
514
+ {
515
+ "name": "invalid_success_json",
516
+ "args": [
517
+ "test",
518
+ 1,
519
+ {}
520
+ ],
521
+ "response": [
522
+ "200",
523
+ "invalid"
524
+ ],
525
+ "client_args": [
526
+ "test-api-key",
527
+ "test-secret",
528
+ "https://example.invalid"
529
+ ],
530
+ "request": {
531
+ "url": "https://example.invalid/Data/add",
532
+ "payload": {
533
+ "api_key": "test-api-key",
534
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
535
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
536
+ },
537
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
538
+ },
539
+ "error": {
540
+ "class": "JSON::ParserError"
541
+ }
542
+ },
543
+ {
544
+ "name": "malformed_error_json",
545
+ "args": [
546
+ "test",
547
+ 1,
548
+ {}
549
+ ],
550
+ "response": [
551
+ "500",
552
+ "{}"
553
+ ],
554
+ "client_args": [
555
+ "test-api-key",
556
+ "test-secret",
557
+ "https://example.invalid"
558
+ ],
559
+ "request": {
560
+ "url": "https://example.invalid/Data/add",
561
+ "payload": {
562
+ "api_key": "test-api-key",
563
+ "sign": "516a0abd16e1406cc5289ec0f8cd268ff6823e17",
564
+ "event": "{\"level\":1,\"spiderName\":\"test\",\"hash\":\"99914b932bd37a50b983c5e7c90ae93b\",\"data\":{}}"
565
+ },
566
+ "body": "api_key=test-api-key&sign=516a0abd16e1406cc5289ec0f8cd268ff6823e17&event=%7B%22level%22%3A1%2C%22spiderName%22%3A%22test%22%2C%22hash%22%3A%2299914b932bd37a50b983c5e7c90ae93b%22%2C%22data%22%3A%7B%7D%7D"
567
+ },
568
+ "error": {
569
+ "class": "NoMethodError"
570
+ }
571
+ },
572
+ {
573
+ "name": "hash_generation_error",
574
+ "args": [
575
+ "test",
576
+ 1,
577
+ null
578
+ ],
579
+ "bad_data": true,
580
+ "client_args": [
581
+ "test-api-key",
582
+ "test-secret",
583
+ "https://example.invalid"
584
+ ],
585
+ "response": [
586
+ "200",
587
+ "{\"ok\":true,\"id\":17,\"nested\":{\"a\":[1,null]}}"
588
+ ],
589
+ "request": null,
590
+ "error": {
591
+ "class": "ShinyError",
592
+ "message": "Fail to generate hash."
593
+ }
594
+ }
595
+ ]
@@ -0,0 +1,220 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/mock'
3
+ require_relative '../lib/shiny-sdk'
4
+
5
+ class ShinySDKTest < Minitest::Test
6
+ Response = Struct.new(:code, :body)
7
+
8
+ class HTTPStub
9
+ attr_accessor :use_ssl
10
+ attr_reader :requests
11
+
12
+ def initialize(response)
13
+ @response = response
14
+ @requests = []
15
+ end
16
+
17
+ def start
18
+ yield self
19
+ end
20
+
21
+ def request(request)
22
+ @requests << request
23
+ @response
24
+ end
25
+ end
26
+
27
+ def setup
28
+ @client = Shiny.new('test-api-key', 'test-secret', 'https://example.invalid')
29
+ @response = Response.new('200', '{"ok":true}')
30
+ end
31
+
32
+ # Captured from the original add method at e2d6c32, before these fixes.
33
+ # In particular, event JSON and form bodies are byte exact.
34
+ JSON.parse(File.read(File.join(__dir__, 'fixtures', 'add_compatibility.json'), encoding: 'UTF-8')).each do |example|
35
+ define_method("test_add_compatibility_#{example.fetch('name')}") do
36
+ client = Shiny.new(*example.fetch('client_args'))
37
+ args = example.fetch('args')
38
+ if example['bad_data']
39
+ bad_data = Object.new
40
+ def bad_data.to_json(*)
41
+ raise ArgumentError, 'cannot serialize'
42
+ end
43
+ args[2] = bad_data
44
+ end
45
+ response = Response.new(*example.fetch('response'))
46
+ captured = nil
47
+ post = lambda do |uri, payload|
48
+ request = Net::HTTP::Post.new(uri)
49
+ request.set_form_data(payload)
50
+ captured = { 'url' => uri.to_s, 'payload' => payload, 'body' => request.body }
51
+ response
52
+ end
53
+ Net::HTTP.stub(:post_form, post) do
54
+ invoke = lambda do
55
+ if example.key?('channel')
56
+ client.add(*args, channel: example['channel'])
57
+ else
58
+ client.add(*args)
59
+ end
60
+ end
61
+ if example.key?('error')
62
+ error = assert_raises(Object.const_get(example['error']['class']), &invoke)
63
+ assert_equal example['error']['message'], error.message if example['error'].key?('message')
64
+ else
65
+ assert_equal example.fetch('result'), invoke.call
66
+ end
67
+ end
68
+ assert_equal example.fetch('request'), captured unless example['request'].nil?
69
+ assert_nil captured if example['request'].nil?
70
+ end
71
+ end
72
+
73
+ def test_sign_hashes_sorted_values_without_mutating_payload
74
+ payload = { status: 'done', jobId: 'job-1' }
75
+ original = payload.dup
76
+ assert_equal Digest::SHA1.hexdigest('test-api-keytest-secretjob-1done'), @client.sign(payload)
77
+ assert_equal original, payload
78
+ assert_equal @client.sign(payload), @client.sign(payload.to_a.reverse.to_h)
79
+ refute_includes @client.sign(payload), 'test-secret'
80
+ end
81
+
82
+ def test_sign_without_parameters
83
+ assert_equal Digest::SHA1.hexdigest('test-api-keytest-secret'), @client.sign
84
+ end
85
+
86
+ def test_add_many_generates_hash_and_normalizes_level
87
+ events = [{ spiderName: 'test', level: '2', data: { b: 2, a: 1 } }]
88
+ capture_post(-> { @client.add_many(events) }) do |uri, payload, result|
89
+ assert_equal 'https://example.invalid/Data/add', uri.to_s
90
+ assert_equal({ 'ok' => true }, result)
91
+ event = JSON.parse(payload.fetch(:event)).first
92
+ assert_equal 2, event.fetch('level')
93
+ assert_equal({ 'b' => 2, 'a' => 1 }, event.fetch('data'))
94
+ assert_equal Digest::MD5.hexdigest([['a', 1], ['b', 2]].to_json), event.fetch('hash')
95
+ assert_equal Digest::SHA1.hexdigest('test-api-keytest-secret' + payload[:event]), payload[:sign]
96
+ assert_equal 'test-api-key', payload[:api_key]
97
+ refute_includes payload[:sign], 'test-secret'
98
+ end
99
+ end
100
+
101
+ def test_add_many_uses_explicit_and_embedded_hashes
102
+ events = [
103
+ { spiderName: 'first', level: '1', data: { hash: 'ignored', message: 'one' }, hash: 'explicit' },
104
+ { spiderName: 'second', level: '2', data: { hash: 'embedded', message: 'two' } }
105
+ ]
106
+ capture_post(-> { @client.add_many(events) }) do |_uri, payload, _result|
107
+ first, second = JSON.parse(payload.fetch(:event))
108
+ assert_equal 'explicit', first.fetch('hash')
109
+ assert_equal 1, first.fetch('level')
110
+ assert_equal 'ignored', first.fetch('data').fetch('hash')
111
+ assert_equal 'embedded', second.fetch('hash')
112
+ assert_equal 2, second.fetch('level')
113
+ assert_equal({ 'message' => 'two' }, second.fetch('data'))
114
+ end
115
+ end
116
+
117
+ def test_add_many_rejects_missing_data_before_sending
118
+ [{ spiderName: 'test', level: 1 }, { spiderName: 'test', level: 1, data: nil }].each do |event|
119
+ Net::HTTP.stub(:post_form, ->(*) { flunk 'must not send invalid events' }) do
120
+ error = assert_raises(ShinyError) { @client.add_many([event]) }
121
+ assert_equal 'Missing parameters.', error.message
122
+ end
123
+ end
124
+ end
125
+
126
+ def test_add_many_accepts_empty_data
127
+ events = [{ spiderName: 'test', level: 1, data: {} }]
128
+ capture_post(-> { @client.add_many(events) }) do |_uri, payload, _result|
129
+ assert_equal({}, JSON.parse(payload.fetch(:event)).first.fetch('data'))
130
+ end
131
+ end
132
+
133
+ def test_get_jobs_initializes_https_and_encodes_credentials
134
+ client = Shiny.new('key+&=? 空', 'test-secret', 'https://example.invalid:8443/api')
135
+ http = HTTPStub.new(Response.new('200', '[{"jobId":"job-1"}]'))
136
+ factory = lambda do |host, port|
137
+ assert_equal 'example.invalid', host
138
+ assert_equal 8443, port
139
+ http
140
+ end
141
+ Net::HTTP.stub(:new, factory) do
142
+ assert_equal [{ 'jobId' => 'job-1' }], client.get_jobs
143
+ end
144
+ assert_equal true, http.use_ssl
145
+ request = http.requests.fetch(0)
146
+ assert_equal 'GET', request.method
147
+ uri = URI.parse(request.path)
148
+ assert_equal '/api/Job/query', uri.path
149
+ assert_equal({ 'api_key' => 'key+&=? 空', 'sign' => Digest::SHA1.hexdigest('key+&=? 空test-secret') }, URI.decode_www_form(uri.query).to_h)
150
+ refute_includes request.path, 'test-secret'
151
+ end
152
+
153
+ def test_get_jobs_supports_http
154
+ client = Shiny.new('key', 'secret', 'http://example.invalid:8080')
155
+ http = HTTPStub.new(@response)
156
+ factory = lambda do |host, port|
157
+ assert_equal ['example.invalid', 8080], [host, port]
158
+ http
159
+ end
160
+ Net::HTTP.stub(:new, factory) do
161
+ assert_equal({ 'ok' => true }, client.get_jobs)
162
+ end
163
+ refute http.use_ssl
164
+ end
165
+
166
+ def test_get_jobs_reports_http_errors
167
+ http = HTTPStub.new(Response.new('503', 'unavailable'))
168
+ Net::HTTP.stub(:new, http) do
169
+ error = assert_raises(ShinyError) { @client.get_jobs }
170
+ assert_equal 'Network error: 503', error.message
171
+ end
172
+ end
173
+
174
+ def test_report_sends_authentication_and_returns_response
175
+ capture_post(-> { @client.report('job-1', 'done') }) do |uri, payload, result|
176
+ assert_equal 'https://example.invalid/Job/report', uri.to_s
177
+ assert_equal({ 'ok' => true }, result)
178
+ assert_equal 'job-1', payload.fetch(:jobId)
179
+ assert_equal 'done', payload.fetch(:status)
180
+ assert_equal 'test-api-key', payload.fetch(:api_key)
181
+ assert_equal Digest::SHA1.hexdigest('test-api-keytest-secretjob-1done'), payload.fetch(:sign)
182
+ refute_includes payload[:sign], 'test-secret'
183
+ end
184
+ end
185
+
186
+ def test_post_methods_preserve_api_errors
187
+ @response = Response.new('403', '{"error":{"info":"invalid signature","code":401}}')
188
+ assert_post_errors 'Shiny error: invalid signature code=401'
189
+ end
190
+
191
+ def test_post_methods_preserve_non_json_http_errors
192
+ @response = Response.new('502', '<html>Bad Gateway</html>')
193
+ assert_post_errors 'Network error: 502'
194
+ end
195
+
196
+ private
197
+
198
+ def capture_post(operation)
199
+ captured = nil
200
+ Net::HTTP.stub(:post_form, ->(uri, payload) { captured = [uri, payload]; @response }) do
201
+ result = operation.call
202
+ refute_nil captured
203
+ yield(*captured, result)
204
+ end
205
+ end
206
+
207
+ def assert_post_errors(message)
208
+ operations = [
209
+ -> { @client.add('test', 1, {}) },
210
+ -> { @client.add_many([{ spiderName: 'test', level: 1, data: {}, hash: 'fixed' }]) },
211
+ -> { @client.report('job-1', 'done') }
212
+ ]
213
+ Net::HTTP.stub(:post_form, @response) do
214
+ operations.each do |operation|
215
+ error = assert_raises(ShinyError, &operation)
216
+ assert_equal message, error.message
217
+ end
218
+ end
219
+ end
220
+ end
metadata CHANGED
@@ -1,31 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shiny-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koell
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-08-15 00:00:00.000000000 Z
12
- dependencies: []
10
+ date: 2026-10-17 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
13
26
  description: Shiny Ruby SDK
14
27
  email: i@wug.moe
15
28
  executables: []
16
29
  extensions: []
17
30
  extra_rdoc_files: []
18
31
  files:
32
+ - ".github/workflows/ci.yml"
33
+ - ".gitignore"
34
+ - Gemfile
19
35
  - LICENSE
20
36
  - README.md
21
37
  - lib/shiny-sdk.rb
22
38
  - shiny-sdk.gemspec
39
+ - test/fixtures/add_compatibility.json
40
+ - test/shiny_sdk_test.rb
23
41
  homepage: https://shiny.kotori.moe
24
42
  licenses:
25
43
  - MIT
26
44
  metadata:
27
45
  source_code_uri: https://github.com/Shiny-Project/Shiny-Ruby-SDK/
28
- post_install_message:
29
46
  rdoc_options: []
30
47
  require_paths:
31
48
  - lib
@@ -40,8 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
57
  - !ruby/object:Gem::Version
41
58
  version: '0'
42
59
  requirements: []
43
- rubygems_version: 3.1.6
44
- signing_key:
60
+ rubygems_version: 4.0.20
45
61
  specification_version: 4
46
62
  summary: Shiny SDK
47
63
  test_files: []