shep 0.1.0.pre.alpha0 → 0.2.1.pre.alpha0
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 +4 -4
- data/README.md +5 -1
- data/Rakefile +1 -0
- data/doc/Shep/Entity/Account.html +22 -37
- data/doc/Shep/Entity/Context.html +8 -9
- data/doc/Shep/Entity/CustomEmoji.html +11 -15
- data/doc/Shep/Entity/MediaAttachment.html +13 -19
- data/doc/Shep/Entity/Notification.html +11 -15
- data/doc/Shep/Entity/Status.html +34 -61
- data/doc/Shep/Entity/StatusSource.html +9 -11
- data/doc/Shep/Entity/Status_Application.html +11 -10
- data/doc/Shep/Entity/Status_Mention.html +10 -13
- data/doc/Shep/Entity/Status_Tag.html +8 -9
- data/doc/Shep/Entity.html +156 -141
- data/doc/Shep/Error/Caller.html +4 -4
- data/doc/Shep/Error/Http.html +22 -22
- data/doc/Shep/Error/RateLimit.html +176 -0
- data/doc/Shep/Error/Remote.html +3 -4
- data/doc/Shep/Error/Server.html +5 -4
- data/doc/Shep/Error/Type.html +10 -12
- data/doc/Shep/Error.html +8 -5
- data/doc/Shep/Session.html +1023 -572
- data/doc/Shep.html +20 -5
- data/doc/_index.html +10 -3
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +52 -33
- data/doc/file_list.html +1 -1
- data/doc/index.html +117 -239
- data/doc/method_list.html +9 -1
- data/doc/top-level-namespace.html +2 -2
- data/lib/shep/entity_base.rb +6 -1
- data/lib/shep/exceptions.rb +8 -0
- data/lib/shep/session.rb +340 -145
- data/lib/shep/version.rb +4 -0
- data/lib/shep.rb +1 -1
- data/run_rake_test.example.sh +12 -5
- data/shep.gemspec +4 -2
- data/spec/session_reader_1_unauth_spec.rb +20 -17
- data/spec/session_reader_2_auth_spec.rb +17 -19
- data/spec/session_writer_spec.rb +4 -11
- data/spec/session_zzz_tricky_spec.rb +205 -0
- data/spec/spec_helper.rb +12 -3
- data/yard_helper.rb +11 -5
- metadata +12 -9
data/lib/shep/version.rb
ADDED
data/lib/shep.rb
CHANGED
data/run_rake_test.example.sh
CHANGED
@@ -5,12 +5,15 @@ set -e
|
|
5
5
|
# needed to run the full suite of tests. Actual values have been
|
6
6
|
# replaced with nonsense.
|
7
7
|
#
|
8
|
-
# If none of these are set, only offline tests will be run
|
8
|
+
# If none of these are set, only offline tests will be run.
|
9
|
+
#
|
10
|
+
# Put your test command on the command line to invoke it; otherwise,
|
11
|
+
# it will run `rake test`.
|
9
12
|
|
10
13
|
|
11
14
|
#
|
12
15
|
# Set these to enable basic read tests:
|
13
|
-
#
|
16
|
+
#
|
14
17
|
|
15
18
|
# The host
|
16
19
|
#export SHEP_TEST_HOST="mastodon.anti.social"
|
@@ -24,7 +27,7 @@ set -e
|
|
24
27
|
# Set these if you also have an account that you wish to use for
|
25
28
|
# testing. This will only read from the account (we hope), but it's
|
26
29
|
# probably not a good idea to use an account you care about.
|
27
|
-
#
|
30
|
+
#
|
28
31
|
|
29
32
|
#export SHEP_ACCOUNT=54321
|
30
33
|
#export SHEP_TOKEN=JDJidvpsdvjapojSDFvfvafWER3qrwDVe4
|
@@ -41,6 +44,10 @@ set -e
|
|
41
44
|
|
42
45
|
#export SHEP_TEST_ALL=1
|
43
46
|
|
44
|
-
|
45
47
|
cd $(dirname "$0")/shep
|
46
|
-
|
48
|
+
|
49
|
+
if [ -z "$1" ]; then
|
50
|
+
rake test
|
51
|
+
else
|
52
|
+
eval "$@"
|
53
|
+
fi
|
data/shep.gemspec
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
require_relative "lib/shep/version"
|
2
|
+
|
1
3
|
Gem::Specification.new do |s|
|
2
4
|
s.name = 'shep'
|
3
|
-
s.version =
|
5
|
+
s.version = Shep::Version
|
4
6
|
s.date = '2023-06-03'
|
5
7
|
s.summary = "An alternate interface to the Mastodon Web API"
|
6
8
|
s.description = <<-EOF
|
@@ -24,5 +26,5 @@ EOF
|
|
24
26
|
s.add_development_dependency 'rake', '~> 13.0.6'
|
25
27
|
|
26
28
|
s.homepage = 'https://codeberg.org/suetanvil/shep'
|
27
|
-
s.license = 'AGPL with linking
|
29
|
+
s.license = 'AGPL-3.0-only with linking exception'
|
28
30
|
end
|
@@ -2,16 +2,16 @@
|
|
2
2
|
require_relative 'spec_helper'
|
3
3
|
require_relative '../lib/shep'
|
4
4
|
|
5
|
+
require "stringio"
|
6
|
+
|
5
7
|
include Shep
|
6
8
|
|
7
9
|
|
8
10
|
describe Session, :anonymous do
|
9
11
|
include_context :uses_temp_dir
|
10
12
|
|
11
|
-
|
12
|
-
|
13
13
|
it "retrieves account information." do
|
14
|
-
ss = Session.new(host: HOST)
|
14
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
15
15
|
acct = ss.fetch_account(READ_ACCOUNT)
|
16
16
|
|
17
17
|
expect( acct.class ) .to be Entity::Account
|
@@ -24,7 +24,7 @@ describe Session, :anonymous do
|
|
24
24
|
end
|
25
25
|
|
26
26
|
it "keeps track of rate limit information." do
|
27
|
-
ss = Session.new(host: HOST)
|
27
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
28
28
|
acct = ss.fetch_account(READ_ACCOUNT)
|
29
29
|
|
30
30
|
expect( ss.rate_limit.remaining ) .to be <= ss.rate_limit.limit
|
@@ -34,7 +34,7 @@ describe Session, :anonymous do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
it "retrieves followers and following accounts." do
|
37
|
-
ss = Session.new(host: HOST)
|
37
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
38
38
|
|
39
39
|
following = ss.each_following(READ_ACCOUNT, limit: 10).to_a
|
40
40
|
expect( following.size ) .to be <= 10
|
@@ -47,7 +47,7 @@ describe Session, :anonymous do
|
|
47
47
|
end
|
48
48
|
|
49
49
|
it "retrieves statuses." do
|
50
|
-
ss = Session.new(host: HOST)
|
50
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
51
51
|
|
52
52
|
sts_all = ss.each_status(READ_ACCOUNT, limit: 50).to_a
|
53
53
|
expect( sts_all.size ) .to be > 0
|
@@ -76,15 +76,20 @@ describe Session, :anonymous do
|
|
76
76
|
|
77
77
|
|
78
78
|
it "does more account stuff." do
|
79
|
-
ss = Session.new(host: HOST)
|
79
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
80
80
|
|
81
81
|
acct1 = ss.fetch_account_by_username(READ_ACCOUNT_HANDLE)
|
82
82
|
acct2 = ss.fetch_account(acct1.id)
|
83
83
|
expect( acct1 ) .to eq acct2
|
84
|
+
|
85
|
+
# If it's missing, you should get nil back. (Funny prank idea: create
|
86
|
+
# this account just to mess with me.)
|
87
|
+
bogus = ss.fetch_account_by_username("bogus_account_not_here_alsidjfals")
|
88
|
+
expect( bogus ) .to be nil
|
84
89
|
end
|
85
90
|
|
86
91
|
it "fetches individual statuses." do
|
87
|
-
ss = Session.new(host: HOST)
|
92
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
88
93
|
|
89
94
|
count = 0
|
90
95
|
ss.each_status(READ_ACCOUNT, limit: 5) {|ref_status|
|
@@ -96,7 +101,7 @@ describe Session, :anonymous do
|
|
96
101
|
end
|
97
102
|
|
98
103
|
it "leaves optional session fields nil if no token is given." do
|
99
|
-
ss = Session.new(host: HOST)
|
104
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
100
105
|
statuses = ss.each_status(READ_ACCOUNT, limit: 1).to_a
|
101
106
|
|
102
107
|
expect( statuses.size ) .to be > 0
|
@@ -110,7 +115,7 @@ describe Session, :anonymous do
|
|
110
115
|
end
|
111
116
|
|
112
117
|
it "fetches status contexts." do
|
113
|
-
ss = Session.new(host: HOST)
|
118
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
114
119
|
|
115
120
|
# Search for the first status to have a reply
|
116
121
|
match_status = nil
|
@@ -137,7 +142,7 @@ describe Session, :anonymous do
|
|
137
142
|
end
|
138
143
|
|
139
144
|
it "fetches accounts that boosted and/or favourited a status." do
|
140
|
-
ss = Session.new(host: HOST)
|
145
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
141
146
|
|
142
147
|
status_count = 0
|
143
148
|
boosted_found = 0
|
@@ -175,9 +180,8 @@ describe Session, :anonymous do
|
|
175
180
|
expect( boosters_found ) .to be > 0
|
176
181
|
end
|
177
182
|
|
178
|
-
|
179
183
|
it "retrieves groups of items via block as well as enumerator." do
|
180
|
-
ss = Session.new(host: HOST)
|
184
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
181
185
|
|
182
186
|
count = 0
|
183
187
|
ss.each_following(READ_ACCOUNT, limit: 10) { count += 1 }
|
@@ -192,9 +196,8 @@ describe Session, :anonymous do
|
|
192
196
|
expect( count ) .to be > 0
|
193
197
|
end
|
194
198
|
|
195
|
-
# TODO: move to own describe block.
|
196
199
|
it "retrieves status' media attachments." do
|
197
|
-
ss = Session.new(host: HOST)
|
200
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
198
201
|
|
199
202
|
# Find the earliest status with one or more media attachments
|
200
203
|
candidate = nil
|
@@ -242,7 +245,7 @@ describe Session, :anonymous do
|
|
242
245
|
|
243
246
|
|
244
247
|
it "retrieves the site's public timeline." do
|
245
|
-
ss = Session.new(host: HOST)
|
248
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
246
249
|
|
247
250
|
caturday = "caturday"
|
248
251
|
|
@@ -282,7 +285,7 @@ describe Session, :anonymous do
|
|
282
285
|
it "retrieves posts by tag." do
|
283
286
|
caturday = "caturday"
|
284
287
|
|
285
|
-
ss = Session.new(host: HOST)
|
288
|
+
ss = Session.new(host: HOST, ua_comment: "(integration test)")
|
286
289
|
|
287
290
|
# Find the latest 100 toots. This assumes an average distribution
|
288
291
|
# that may not happen on the actual live server, in which case
|
@@ -7,8 +7,9 @@ require_relative '../lib/shep'
|
|
7
7
|
include Shep
|
8
8
|
|
9
9
|
describe Session, :account do
|
10
|
+
|
10
11
|
it "fetches status sources." do
|
11
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
12
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
12
13
|
statuses = ss.each_status(WRITE_ACCOUNT, limit: 1).to_a
|
13
14
|
expect( statuses.empty? ) .to be false
|
14
15
|
stat0 = statuses[0]
|
@@ -22,13 +23,13 @@ describe Session, :account do
|
|
22
23
|
end
|
23
24
|
|
24
25
|
it "reads the home timeline" do
|
25
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
26
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
26
27
|
statuses = ss.each_home_status(limit: 10).to_a
|
27
28
|
expect( statuses.size ) .to be > 0
|
28
29
|
end
|
29
30
|
|
30
31
|
it "reads notifications" do
|
31
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
32
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
32
33
|
|
33
34
|
ntfns = ss.each_notification(limit: 10).to_a
|
34
35
|
expect( ntfns.size ) .to be > 0
|
@@ -72,25 +73,22 @@ describe Session, :account do
|
|
72
73
|
}.call
|
73
74
|
end
|
74
75
|
|
75
|
-
|
76
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
76
|
+
it "timeline retrivals support max_id:" do
|
77
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
range_fn = proc { |&action|
|
80
|
+
group_1 = action.call(limit: 10).to_a
|
81
|
+
expect(group_1.size) .to eq 10 # not an error, but we can't continue if false
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
first = ntfns[0]
|
83
|
+
group_2 = action.call(limit: 5, max_id: group_1[4].id).to_a
|
84
|
+
expect( group_2.size ) .to be > 0 # not an error but...
|
85
85
|
|
86
|
-
|
86
|
+
expect( group_2 ) .to eq group_1[5..]
|
87
|
+
}
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
range_fn.call{|**kwargs| ss.each_status(READ_ACCOUNT, **kwargs)}
|
90
|
+
range_fn.call{|**kwargs| ss.each_public_status(**kwargs)}
|
91
|
+
range_fn.call{|**kwargs| ss.each_tag_status("caturday", **kwargs)}
|
92
|
+
range_fn.call{|**kwargs| ss.each_home_status(**kwargs)}
|
91
93
|
end
|
92
|
-
|
93
|
-
|
94
|
-
# skip "" do ; end
|
95
|
-
|
96
94
|
end
|
data/spec/session_writer_spec.rb
CHANGED
@@ -6,8 +6,6 @@ require_relative '../lib/shep'
|
|
6
6
|
|
7
7
|
include Shep
|
8
8
|
|
9
|
-
POST_UUID = "fc49e486-d445-4ca7-9d6d-a27c89c5c471" #SecureRandom.uuid
|
10
|
-
|
11
9
|
describe Session, :account_limited do
|
12
10
|
include_context :uses_temp_dir
|
13
11
|
|
@@ -18,7 +16,7 @@ describe Session, :account_limited do
|
|
18
16
|
it "posts statuses" do
|
19
17
|
testpost = "Test post #{POST_UUID} "
|
20
18
|
|
21
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
19
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
22
20
|
|
23
21
|
visibilities = %w{public unlisted private direct}
|
24
22
|
for vis in visibilities
|
@@ -46,7 +44,7 @@ describe Session, :account_limited do
|
|
46
44
|
end
|
47
45
|
|
48
46
|
it "posts status with media" do
|
49
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
47
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
50
48
|
|
51
49
|
file1 = File.join(File.dirname(__FILE__), "data", "smallimg.jpg")
|
52
50
|
file2 = File.join(File.dirname(__FILE__), "data", "smallish.jpg")
|
@@ -82,7 +80,7 @@ describe Session, :account_limited do
|
|
82
80
|
end
|
83
81
|
|
84
82
|
it "edits statuses" do
|
85
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
83
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
86
84
|
|
87
85
|
base_text = "edited msg #{POST_UUID}\n#{Time.now}\n-"
|
88
86
|
post = ss.post_status(base_text, language: "en")
|
@@ -160,7 +158,7 @@ describe Session, :account_limited do
|
|
160
158
|
end
|
161
159
|
|
162
160
|
it "deletes statuses" do
|
163
|
-
ss = Session.new(host: HOST, token: TOKEN)
|
161
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
164
162
|
|
165
163
|
# Test for the test itself
|
166
164
|
expect( @new_posts.size ) .to be > 0
|
@@ -175,9 +173,4 @@ describe Session, :account_limited do
|
|
175
173
|
|
176
174
|
@new_posts = []
|
177
175
|
end
|
178
|
-
|
179
|
-
|
180
|
-
# skip "" do ; end
|
181
|
-
# skip "" do ; end
|
182
|
-
|
183
176
|
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
|
2
|
+
# These are the things that are hard or slow to test, so they need to
|
3
|
+
# be run by hand, possibly with some external intervention.
|
4
|
+
|
5
|
+
|
6
|
+
require_relative 'spec_helper'
|
7
|
+
require_relative '../lib/shep'
|
8
|
+
|
9
|
+
include Shep
|
10
|
+
|
11
|
+
|
12
|
+
# Test `dismiss_notification`; this permanently dismisses the most
|
13
|
+
# recent notification so you will need to first confirm that a)
|
14
|
+
# there's a notification present and b) it's one you don't care about.
|
15
|
+
describe Session, :tricky_del_notification do
|
16
|
+
it "expects to be configured for writing" do
|
17
|
+
expect(WRITE_ACCOUNT) .to_not be nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "dismisses notifications" do
|
21
|
+
ss = Session.new(host: HOST, token: TOKEN, ua_comment: "(integration test)")
|
22
|
+
|
23
|
+
# We dismiss the first notification, then confirm that it is no
|
24
|
+
# longer there. Since this is not repeatable, we skip it by
|
25
|
+
# default and manually enable it when we want to test it.
|
26
|
+
|
27
|
+
ntfns = ss.each_notification(limit: 10).to_a
|
28
|
+
expect( ntfns.size ) .to be > 0
|
29
|
+
first = ntfns[0]
|
30
|
+
|
31
|
+
ss.dismiss_notification(first.id)
|
32
|
+
|
33
|
+
ntfns2 = ss.each_notification(limit: 10).to_a
|
34
|
+
expect( ntfns2.size ) .to eq ntfns.size - 1
|
35
|
+
expect( ntfns2[0] ) .to_not eq first
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# These tests exhaust the ratelimit of the delete API endpoint several
|
41
|
+
# times. Running these two tests takes a long time and leaves us with
|
42
|
+
# no more deletes.
|
43
|
+
describe Session, :tricky_ratelimit do
|
44
|
+
|
45
|
+
it "expects to be configured for writing" do
|
46
|
+
expect(WRITE_ACCOUNT) .to_not be nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "retries when the rate limit is reached" do
|
50
|
+
# Enables retry mode and sets rlhook as the hook function to call,
|
51
|
+
# then posts and deletes status until the hook as been called.
|
52
|
+
# Afterward, we make one more try.
|
53
|
+
|
54
|
+
rlhook_called = false
|
55
|
+
rlhook = proc do |rl|
|
56
|
+
rlhook_called = true
|
57
|
+
|
58
|
+
delay = (rl.reset - Time.now).round
|
59
|
+
STDOUT.puts "Waiting #{delay}s for rate limit to reset."
|
60
|
+
|
61
|
+
# We stop at the 3 second mark so the underlying delay will also
|
62
|
+
# kick in.
|
63
|
+
while rl.reset - 3 >= Time.now
|
64
|
+
STDOUT.print "\r #{(rl.reset - Time.now).round} seconds "
|
65
|
+
STDOUT.flush
|
66
|
+
sleep 1
|
67
|
+
end
|
68
|
+
|
69
|
+
STDOUT.puts "\nDone!"
|
70
|
+
end
|
71
|
+
|
72
|
+
ss = Session.new(host: HOST,
|
73
|
+
token: TOKEN,
|
74
|
+
ua_comment: "(integration test; rate limit reached)",
|
75
|
+
|
76
|
+
rate_limit_retry: true,
|
77
|
+
retry_hook: rlhook,
|
78
|
+
|
79
|
+
logger: :info
|
80
|
+
)
|
81
|
+
|
82
|
+
post_and_del = proc do
|
83
|
+
testpost = "test post for deleting9 #{POST_UUID} #{Time.now}"
|
84
|
+
post = ss.post_status(testpost)
|
85
|
+
delpost = ss.delete_status(post.id)
|
86
|
+
|
87
|
+
expect( post.id ) .to eq delpost.id
|
88
|
+
|
89
|
+
STDOUT.print "."
|
90
|
+
STDOUT.flush
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
limit = nil
|
95
|
+
count = 0
|
96
|
+
while !rlhook_called
|
97
|
+
post_and_del.call
|
98
|
+
|
99
|
+
# First pass, we fetch the deletion limit
|
100
|
+
limit = ss.rate_limit.limit unless limit
|
101
|
+
|
102
|
+
count += 1
|
103
|
+
expect( count ) .to be < (2*limit + 1)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Ensure we did at least one iteration
|
107
|
+
expect( count ) .to be >= 1
|
108
|
+
expect( rlhook_called ) .to be true
|
109
|
+
|
110
|
+
# And ensure that we can do another post/delete cycle
|
111
|
+
post_and_del.call
|
112
|
+
expect( ss.rate_limit.remaining ) .to be > 0
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# This test will deliberately exhaust the deletion rate limit.
|
117
|
+
it "throws Error::RateLimit when rate limit is exhusted" do
|
118
|
+
# We post and delete the post until the ratelimit is exhausted, at
|
119
|
+
# which point Session should throw a Shep::Error::RateLimit
|
120
|
+
|
121
|
+
ss = Session.new(host: HOST,
|
122
|
+
token: TOKEN,
|
123
|
+
ua_comment: "(integration test; rate limit reached)")
|
124
|
+
|
125
|
+
post_and_del = proc{
|
126
|
+
testpost = "test post #{POST_UUID} #{Time.now}"
|
127
|
+
post = ss.post_status(testpost)
|
128
|
+
delpost = ss.delete_status(post.id)
|
129
|
+
|
130
|
+
expect( post.id ) .to eq delpost.id
|
131
|
+
}
|
132
|
+
|
133
|
+
expect { 50.times{ post_and_del.call } }
|
134
|
+
.to raise_error(Shep::Error::RateLimit)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# Ratelimit retry is different for media uploads because the media
|
140
|
+
# file is an IO-like object that gets read as part of the upload.
|
141
|
+
# This test ensures that this also works.
|
142
|
+
#
|
143
|
+
# (It does this by uploading the same image file over and over again.
|
144
|
+
# It's a small file though, so hopefully that will minimize the stress
|
145
|
+
# we put on the server.)
|
146
|
+
describe Session, :tricky_ratelimit_media_upload do
|
147
|
+
it "expects to be configured for writing" do
|
148
|
+
expect(WRITE_ACCOUNT) .to_not be nil
|
149
|
+
end
|
150
|
+
|
151
|
+
it "rate-limit retry works with media uploads too" do
|
152
|
+
|
153
|
+
rlhook_called = false
|
154
|
+
rlhook = proc do |rl|
|
155
|
+
rlhook_called = true
|
156
|
+
|
157
|
+
delay = (rl.reset - Time.now).round
|
158
|
+
STDOUT.puts "Waiting #{delay}s for rate limit to reset."
|
159
|
+
|
160
|
+
# We stop at the 3 second mark so the underlying delay will also
|
161
|
+
# kick in.
|
162
|
+
while rl.reset - 3 >= Time.now
|
163
|
+
STDOUT.print "\r #{(rl.reset - Time.now).round} seconds "
|
164
|
+
STDOUT.flush
|
165
|
+
sleep 1
|
166
|
+
end
|
167
|
+
|
168
|
+
STDOUT.puts "\nDone!"
|
169
|
+
end
|
170
|
+
|
171
|
+
ss = Session.new(host: HOST,
|
172
|
+
token: TOKEN,
|
173
|
+
ua_comment: "(integration test; rate limit reached)",
|
174
|
+
|
175
|
+
rate_limit_retry: true,
|
176
|
+
retry_hook: rlhook,
|
177
|
+
|
178
|
+
logger: :info
|
179
|
+
)
|
180
|
+
|
181
|
+
media_file = File.join(File.dirname(__FILE__), "data", "smallimg.jpg")
|
182
|
+
|
183
|
+
limit = nil
|
184
|
+
count = 0
|
185
|
+
while !rlhook_called
|
186
|
+
ma = ss.upload_media(media_file)
|
187
|
+
|
188
|
+
# First pass, we fetch the deletion limit
|
189
|
+
limit = ss.rate_limit.limit unless limit
|
190
|
+
|
191
|
+
STDOUT.print "."
|
192
|
+
STDOUT.flush
|
193
|
+
|
194
|
+
count += 1
|
195
|
+
end
|
196
|
+
|
197
|
+
# Ensure we did at least one iteration
|
198
|
+
expect( rlhook_called ) .to be true
|
199
|
+
expect( count ) .to be >= 1
|
200
|
+
|
201
|
+
# And ensure that we can do another post/delete cycle
|
202
|
+
ma = ss.upload_media(media_file)
|
203
|
+
expect( ma.id ) .to_not be nil # kind of redundant, but whatever
|
204
|
+
end
|
205
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -20,13 +20,22 @@ WRITE_ACCOUNT = ENV['SHEP_ACCOUNT']
|
|
20
20
|
ALL_TESTS_REQUESTED = !!ENV['SHEP_TEST_ALL']
|
21
21
|
|
22
22
|
|
23
|
+
|
24
|
+
# UUID to put in test posts so we can identify them when testing deletion.
|
25
|
+
POST_UUID = "fc49e486-d445-4ca7-9d6d-a27c89c5c471" #SecureRandom.uuid
|
26
|
+
# Currently a constant so that I can clean up after broken tests.
|
27
|
+
# Ideally, this should be different for each run.
|
28
|
+
|
29
|
+
|
23
30
|
# Tags:
|
24
31
|
#
|
25
|
-
# offline - runs without a Mastodon server
|
32
|
+
# offline - runs without a Mastodon server (always on)
|
26
33
|
# anonymous - requires a server but no login
|
27
34
|
# account - requires normal server account (which it *may* modify!)
|
28
35
|
# account_limited - like 'account' but will also perform heavily
|
29
36
|
# rate-limited actions (e.g. delete, upload media)
|
37
|
+
#
|
38
|
+
#
|
30
39
|
|
31
40
|
|
32
41
|
RSpec.configure {|conf|
|
@@ -52,8 +61,8 @@ RSpec.configure {|conf|
|
|
52
61
|
puts
|
53
62
|
|
54
63
|
conf.filter_run_including(:offline)
|
55
|
-
conf.filter_run_including(:anonymous)
|
56
|
-
conf.filter_run_including(:account)
|
64
|
+
conf.filter_run_including(:anonymous) if allow_anonymous
|
65
|
+
conf.filter_run_including(:account) if allow_account
|
57
66
|
conf.filter_run_including(:account_limited) if allow_limited
|
58
67
|
end
|
59
68
|
|
data/yard_helper.rb
CHANGED
@@ -6,6 +6,11 @@ class EntityFieldDeclHandler < YARD::Handlers::Ruby::Base #AttributeHandler
|
|
6
6
|
namespace_only
|
7
7
|
|
8
8
|
process do
|
9
|
+
sanitized = {
|
10
|
+
'<' => '<',
|
11
|
+
'>' => '>',
|
12
|
+
}
|
13
|
+
|
9
14
|
mastotype = namespace.name.to_s.gsub(/_/, '::')
|
10
15
|
new_docstr = "Ruby object representing a Mastodon `#{mastotype}` object:\n\n"
|
11
16
|
|
@@ -15,9 +20,13 @@ class EntityFieldDeclHandler < YARD::Handlers::Ruby::Base #AttributeHandler
|
|
15
20
|
# completely done fighting with YARD to get it to recognize my
|
16
21
|
# 'attr' tags.
|
17
22
|
|
18
|
-
|
23
|
+
typename = type.to_yard_s
|
24
|
+
.gsub(/[<>]/) {|match| sanitized[match]} # Excape angle brackets
|
25
|
+
.gsub(/(Entity::\w+)/, '{\1}') # So YARD can generate refs
|
26
|
+
|
27
|
+
new_docstr += " * #{name} (#{typename})\n"
|
19
28
|
end
|
20
|
-
|
29
|
+
|
21
30
|
new_docstr += "\n"
|
22
31
|
new_docstr += "See `Shep::Entity` for an overview.\n\n"
|
23
32
|
new_docstr += namespace.docstring.to_raw.strip
|
@@ -25,6 +34,3 @@ class EntityFieldDeclHandler < YARD::Handlers::Ruby::Base #AttributeHandler
|
|
25
34
|
namespace.docstring.replace(new_docstr)
|
26
35
|
end
|
27
36
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|