shep 0.1.0.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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/Copyright.txt +8 -0
  3. data/LICENSE.txt +697 -0
  4. data/README.md +101 -0
  5. data/Rakefile +52 -0
  6. data/doc/Shep/Entity/Account.html +193 -0
  7. data/doc/Shep/Entity/Context.html +165 -0
  8. data/doc/Shep/Entity/CustomEmoji.html +171 -0
  9. data/doc/Shep/Entity/MediaAttachment.html +175 -0
  10. data/doc/Shep/Entity/Notification.html +171 -0
  11. data/doc/Shep/Entity/Status.html +217 -0
  12. data/doc/Shep/Entity/StatusSource.html +167 -0
  13. data/doc/Shep/Entity/Status_Application.html +167 -0
  14. data/doc/Shep/Entity/Status_Mention.html +169 -0
  15. data/doc/Shep/Entity/Status_Tag.html +165 -0
  16. data/doc/Shep/Entity.html +1457 -0
  17. data/doc/Shep/Error/Caller.html +147 -0
  18. data/doc/Shep/Error/Http.html +329 -0
  19. data/doc/Shep/Error/Remote.html +143 -0
  20. data/doc/Shep/Error/Server.html +147 -0
  21. data/doc/Shep/Error/Type.html +233 -0
  22. data/doc/Shep/Error.html +149 -0
  23. data/doc/Shep/Session.html +4094 -0
  24. data/doc/Shep.html +128 -0
  25. data/doc/_index.html +300 -0
  26. data/doc/class_list.html +51 -0
  27. data/doc/css/common.css +1 -0
  28. data/doc/css/full_list.css +58 -0
  29. data/doc/css/style.css +497 -0
  30. data/doc/file.README.html +159 -0
  31. data/doc/file_list.html +56 -0
  32. data/doc/frames.html +17 -0
  33. data/doc/index.html +300 -0
  34. data/doc/js/app.js +314 -0
  35. data/doc/js/full_list.js +216 -0
  36. data/doc/js/jquery.js +4 -0
  37. data/doc/method_list.html +387 -0
  38. data/doc/top-level-namespace.html +110 -0
  39. data/lib/shep/entities.rb +164 -0
  40. data/lib/shep/entity_base.rb +378 -0
  41. data/lib/shep/exceptions.rb +78 -0
  42. data/lib/shep/session.rb +970 -0
  43. data/lib/shep/typeboxes.rb +180 -0
  44. data/lib/shep.rb +22 -0
  45. data/run_rake_test.example.sh +46 -0
  46. data/shep.gemspec +28 -0
  47. data/spec/data/smallimg.jpg +0 -0
  48. data/spec/data/smallish.jpg +0 -0
  49. data/spec/entity_common.rb +120 -0
  50. data/spec/entity_t1_spec.rb +168 -0
  51. data/spec/entity_t2_spec.rb +123 -0
  52. data/spec/entity_t3_spec.rb +30 -0
  53. data/spec/json_objects/account.1.json +25 -0
  54. data/spec/json_objects/account.2.json +36 -0
  55. data/spec/json_objects/status.1.json +85 -0
  56. data/spec/json_objects/status.2.json +59 -0
  57. data/spec/json_objects/status.3.json +95 -0
  58. data/spec/json_objects/status.4.json +95 -0
  59. data/spec/json_objects/status.5.json +74 -0
  60. data/spec/json_objects/status.6.json +140 -0
  61. data/spec/json_objects/status.7.json +84 -0
  62. data/spec/session_reader_1_unauth_spec.rb +366 -0
  63. data/spec/session_reader_2_auth_spec.rb +96 -0
  64. data/spec/session_writer_spec.rb +183 -0
  65. data/spec/spec_helper.rb +73 -0
  66. data/yard_helper.rb +30 -0
  67. metadata +154 -0
@@ -0,0 +1,84 @@
1
+ {
2
+ "id": "110407677589865707",
3
+ "created_at": "2023-05-21T16:44:23.237Z",
4
+ "in_reply_to_id": null,
5
+ "in_reply_to_account_id": null,
6
+ "sensitive": false,
7
+ "spoiler_text": "",
8
+ "visibility": "unlisted",
9
+ "language": "en",
10
+ "uri": "https://botsin.space/users/testingtesting123/statuses/110407677589865707",
11
+ "url": "https://botsin.space/@testingtesting123/110407677589865707",
12
+ "replies_count": 0,
13
+ "reblogs_count": 0,
14
+ "favourites_count": 0,
15
+ "edited_at": null,
16
+ "favourited": false,
17
+ "reblogged": false,
18
+ "muted": false,
19
+ "bookmarked": false,
20
+ "pinned": false,
21
+ "content": "<p>This is a test</p><p><a href=\"https://botsin.space/tags/with\" class=\"mention hashtag\" rel=\"tag\">#<span>with</span></a> <a href=\"https://botsin.space/tags/some\" class=\"mention hashtag\" rel=\"tag\">#<span>some</span></a> <a href=\"https://botsin.space/tags/tags\" class=\"mention hashtag\" rel=\"tag\">#<span>tags</span></a> <a href=\"https://botsin.space/tags/for\" class=\"mention hashtag\" rel=\"tag\">#<span>for</span></a> <a href=\"https://botsin.space/tags/no\" class=\"mention hashtag\" rel=\"tag\">#<span>no</span></a> <a href=\"https://botsin.space/tags/reason\" class=\"mention hashtag\" rel=\"tag\">#<span>reason</span></a></p>",
22
+ "filtered": [],
23
+ "reblog": null,
24
+ "application": {
25
+ "name": "Web",
26
+ "website": null
27
+ },
28
+ "account": {
29
+ "id": "22411",
30
+ "username": "testingtesting123",
31
+ "acct": "testingtesting123",
32
+ "display_name": "scratchmonkey_15310",
33
+ "locked": false,
34
+ "bot": false,
35
+ "discoverable": null,
36
+ "group": false,
37
+ "created_at": "2018-01-20T00:00:00.000Z",
38
+ "note": "<p>Account used for bot testing; nothing to see here.</p><p><span class=\"h-card\"><a href=\"https://i.write.codethat.sucks/@suetanvil\" class=\"u-url mention\">@<span>suetanvil</span></a></span><br /><span class=\"h-card\"><a href=\"https://botsin.space/@benoitmandelbot\" class=\"u-url mention\">@<span>benoitmandelbot</span></a></span></p>",
39
+ "url": "https://botsin.space/@testingtesting123",
40
+ "avatar": "https://files.botsin.space/accounts/avatars/000/022/411/original/a701d93a6a1d0497.gif",
41
+ "avatar_static": "https://files.botsin.space/accounts/avatars/000/022/411/static/a701d93a6a1d0497.png",
42
+ "header": "https://botsin.space/headers/original/missing.png",
43
+ "header_static": "https://botsin.space/headers/original/missing.png",
44
+ "followers_count": 2,
45
+ "following_count": 3,
46
+ "statuses_count": 8,
47
+ "last_status_at": "2023-05-21",
48
+ "noindex": true,
49
+ "emojis": [],
50
+ "roles": [],
51
+ "fields": []
52
+ },
53
+ "media_attachments": [],
54
+ "mentions": [],
55
+ "tags": [
56
+ {
57
+ "name": "with",
58
+ "url": "https://botsin.space/tags/with"
59
+ },
60
+ {
61
+ "name": "some",
62
+ "url": "https://botsin.space/tags/some"
63
+ },
64
+ {
65
+ "name": "tags",
66
+ "url": "https://botsin.space/tags/tags"
67
+ },
68
+ {
69
+ "name": "for",
70
+ "url": "https://botsin.space/tags/for"
71
+ },
72
+ {
73
+ "name": "no",
74
+ "url": "https://botsin.space/tags/no"
75
+ },
76
+ {
77
+ "name": "reason",
78
+ "url": "https://botsin.space/tags/reason"
79
+ }
80
+ ],
81
+ "emojis": [],
82
+ "card": null,
83
+ "poll": null
84
+ }
@@ -0,0 +1,366 @@
1
+
2
+ require_relative 'spec_helper'
3
+ require_relative '../lib/shep'
4
+
5
+ include Shep
6
+
7
+
8
+ describe Session, :anonymous do
9
+ include_context :uses_temp_dir
10
+
11
+
12
+
13
+ it "retrieves account information." do
14
+ ss = Session.new(host: HOST)
15
+ acct = ss.fetch_account(READ_ACCOUNT)
16
+
17
+ expect( acct.class ) .to be Entity::Account
18
+ expect( acct.id ) .to eq READ_ACCOUNT
19
+
20
+ %i{username display_name locked bot created_at followers_count
21
+ following_count} .each {|fld|
22
+ expect( acct[fld] ) .to_not eq nil
23
+ }
24
+ end
25
+
26
+ it "keeps track of rate limit information." do
27
+ ss = Session.new(host: HOST)
28
+ acct = ss.fetch_account(READ_ACCOUNT)
29
+
30
+ expect( ss.rate_limit.remaining ) .to be <= ss.rate_limit.limit
31
+
32
+ # Race condition, but unlikely to be a problem
33
+ expect( ss.rate_limit.reset ) .to be > Time.now
34
+ end
35
+
36
+ it "retrieves followers and following accounts." do
37
+ ss = Session.new(host: HOST)
38
+
39
+ following = ss.each_following(READ_ACCOUNT, limit: 10).to_a
40
+ expect( following.size ) .to be <= 10
41
+ expect( following[0].class ) .to be Entity::Account
42
+ following = nil
43
+
44
+ followers = ss.each_follower(READ_ACCOUNT, limit: 10).to_a
45
+ expect( followers.size ) .to be <= 10
46
+ expect( followers[0].class ) .to be Entity::Account
47
+ end
48
+
49
+ it "retrieves statuses." do
50
+ ss = Session.new(host: HOST)
51
+
52
+ sts_all = ss.each_status(READ_ACCOUNT, limit: 50).to_a
53
+ expect( sts_all.size ) .to be > 0
54
+
55
+ # This is not complete, alas. The test account doesn't have many
56
+ # interactions so stuff like exclude_replies doesn't really change stuff.
57
+ sts_orig = ss.each_status(READ_ACCOUNT, limit: 50,
58
+ exclude_replies: true,
59
+ exclude_reblogs: true
60
+ ).to_a
61
+ expect( sts_orig.size ) .to be > 0
62
+ #expect( sts_orig ) .to_not eq sts_all
63
+
64
+
65
+ sts_pinned = ss.each_status(READ_ACCOUNT, limit: 50,
66
+ pinned: true,
67
+ ).to_a
68
+ expect( sts_pinned.size ) .to be > 0
69
+ expect( sts_pinned ) .to_not eq sts_all
70
+
71
+ # Retrieves statuses when no limit: value is set
72
+ count = 0
73
+ ss.each_status(READ_ACCOUNT) {|stat| count += 1; break if count > 41 }
74
+ expect( count ) .to be > 41
75
+ end
76
+
77
+
78
+ it "does more account stuff." do
79
+ ss = Session.new(host: HOST)
80
+
81
+ acct1 = ss.fetch_account_by_username(READ_ACCOUNT_HANDLE)
82
+ acct2 = ss.fetch_account(acct1.id)
83
+ expect( acct1 ) .to eq acct2
84
+ end
85
+
86
+ it "fetches individual statuses." do
87
+ ss = Session.new(host: HOST)
88
+
89
+ count = 0
90
+ ss.each_status(READ_ACCOUNT, limit: 5) {|ref_status|
91
+ new_status = ss.fetch_status(ref_status.id)
92
+ expect(new_status) .to eq ref_status
93
+ count += 1
94
+ }
95
+ expect(count) .to be > 0
96
+ end
97
+
98
+ it "leaves optional session fields nil if no token is given." do
99
+ ss = Session.new(host: HOST)
100
+ statuses = ss.each_status(READ_ACCOUNT, limit: 1).to_a
101
+
102
+ expect( statuses.size ) .to be > 0
103
+ status = statuses[0]
104
+
105
+ expect( status.favourited ) .to be nil
106
+ expect( status.reblogged ) .to be nil
107
+ expect( status.muted ) .to be nil
108
+ expect( status.bookmarked ) .to be nil
109
+ expect( status.pinned ) .to be nil
110
+ end
111
+
112
+ it "fetches status contexts." do
113
+ ss = Session.new(host: HOST)
114
+
115
+ # Search for the first status to have a reply
116
+ match_status = nil
117
+ match_context = nil
118
+ count = 0
119
+ ss.each_status(READ_ACCOUNT, limit: 500) {|ref_status|
120
+ count += 1
121
+ context = ss.fetch_context(ref_status.id)
122
+ unless context.descendants.empty?
123
+ match_status = ref_status
124
+ match_context = context
125
+ break
126
+ end
127
+ }
128
+ expect( count ) .to be > 0
129
+ expect( !!match_status && !!match_context ) .to be true
130
+
131
+ # Now fetch its descendent and the descendent's ancestors to
132
+ # confirm that the one we found is in it.
133
+ kid = ss.fetch_status(match_context.descendants[0].id)
134
+ kid_ancestors = ss.fetch_context(kid.id).ancestors
135
+
136
+ expect( kid_ancestors.map{|a| a.id} ) .to include(match_status.id)
137
+ end
138
+
139
+ it "fetches accounts that boosted and/or favourited a status." do
140
+ ss = Session.new(host: HOST)
141
+
142
+ status_count = 0
143
+ boosted_found = 0
144
+ boosters_found = 0
145
+ faved_found = 0
146
+ favers_found = 0
147
+ ss.each_status(READ_ACCOUNT, limit: 500) do |status|
148
+ status_count += 1
149
+
150
+ if status.reblogs_count > 0
151
+ boosted_found += 1
152
+ boosts = ss.each_boost_acct(status.id, limit: 10).to_a
153
+ boosters_found += boosts.size
154
+ end
155
+
156
+ if status.favourites_count > 0
157
+ faved_found += 1
158
+ faves = ss.each_fave_acct(status.id, limit: 10).to_a
159
+ favers_found += faves.size
160
+ end
161
+
162
+ break if favers_found > 0 && boosters_found > 0
163
+ end
164
+
165
+ puts "\nfaves/boosts: searched #{status_count} statuses."
166
+
167
+ # This will be false if there are no favourited or no boosted
168
+ # statuses in the searched range of stats. That's not necessarily
169
+ # a bug here, but it's hard to test without assuming this so we
170
+ # test for it.
171
+ expect( faved_found > 0 && boosters_found > 0 ) .to be true
172
+
173
+ # Ditto for these. Depends on account visibility (I *think*)
174
+ expect( favers_found ) .to be > 0
175
+ expect( boosters_found ) .to be > 0
176
+ end
177
+
178
+
179
+ it "retrieves groups of items via block as well as enumerator." do
180
+ ss = Session.new(host: HOST)
181
+
182
+ count = 0
183
+ ss.each_following(READ_ACCOUNT, limit: 10) { count += 1 }
184
+ expect( count ) .to be > 0
185
+
186
+ count = 0
187
+ ss.each_follower(READ_ACCOUNT, limit: 10) { count += 1 }
188
+ expect( count ) .to be > 0
189
+
190
+ count = 0
191
+ ss.each_status(READ_ACCOUNT, limit: 10) { count += 1 }
192
+ expect( count ) .to be > 0
193
+ end
194
+
195
+ # TODO: move to own describe block.
196
+ it "retrieves status' media attachments." do
197
+ ss = Session.new(host: HOST)
198
+
199
+ # Find the earliest status with one or more media attachments
200
+ candidate = nil
201
+ ss.each_status(READ_ACCOUNT, limit: 500) { |stat|
202
+ if stat.media_attachments.size > 0
203
+ candidate = stat
204
+ break
205
+ end
206
+ }
207
+
208
+ # This can happen if there are no statuses with media attachments
209
+ # in READ_ACCOUNT, but in that case we can't test, so
210
+ # :shrug_emoji:
211
+ expect( candidate ) .to_not be nil
212
+
213
+ status, files = ss.fetch_status_with_media(candidate.id, tmp_dir)
214
+ expect( status ) .to eq candidate # BTW
215
+ expect( files.empty? ) .to be false
216
+
217
+ file0 = files.values[0]
218
+ expect( file0 ) .to_not be nil
219
+
220
+ # Ensure the file is present an non-empty. (Technically, it
221
+ # *could* be empty, but :shrug_emoji: as above).
222
+ expect( File.exist?( file0 ) ) .to be true
223
+ fsz = File.size( file0 )
224
+ expect( fsz ) .to be > 0
225
+
226
+ # Test refetch: false; ensure it doesn't overwrite if the file is
227
+ # present.
228
+ File.open(file0, "w") {|fh| } # truncate the file
229
+ expect( File.size( file0 ) ) .to eq 0
230
+
231
+ _, files2 = ss.fetch_status_with_media(candidate.id, tmp_dir, refetch: false)
232
+ expect( files2) .to eq files
233
+
234
+ # No overwrite
235
+ expect( File.size( file0 ) ) .to eq 0
236
+
237
+ # Now, ensure that refetch:false *does* overwrite the file
238
+ _, files3 = ss.fetch_status_with_media(candidate.id, tmp_dir, refetch: true)
239
+ expect( files3) .to eq files
240
+ expect( File.size( file0 ) ) .to eq fsz
241
+ end
242
+
243
+
244
+ it "retrieves the site's public timeline." do
245
+ ss = Session.new(host: HOST)
246
+
247
+ caturday = "caturday"
248
+
249
+ # Find the latest 100 toots. This assumes an average distribution
250
+ # that may not happen on the actual live server, in which case
251
+ # this test will fail for no good reason.
252
+ default_tl = ss.each_public_status(limit: 200).to_a
253
+
254
+ # Some toots are federated
255
+ expect( default_tl.select{|st| st.account.acct.include? '@'}.empty? )
256
+ .to be false
257
+
258
+ # Some toots are local
259
+ expect( default_tl.reject{|st| st.account.acct.include? '@'}.empty? )
260
+ .to be false
261
+
262
+ # Some toots contain media
263
+ expect( default_tl.reject{|st| st.media_attachments.empty? }.empty? )
264
+ .to be false
265
+
266
+ # All statuses should contain media when selecting only_media
267
+ media_tl = ss.each_public_status(limit: 20, only_media: true).to_a;
268
+ expect( media_tl.select{|st| st.media_attachments.empty?} )
269
+ .to eq []
270
+
271
+ # local:true should return no toots from a remote site
272
+ local_tl = ss.each_public_status(limit: 20, local: true).to_a;
273
+ expect( local_tl.select{|st| st.account.acct.include? '@' } )
274
+ .to eq []
275
+
276
+ # remote:true should return no toots from the local site
277
+ remote_tl = ss.each_public_status(limit: 20, remote: true).to_a;
278
+ expect( remote_tl.reject{|st| st.account.acct.include? '@' } )
279
+ .to eq []
280
+ end
281
+
282
+ it "retrieves posts by tag." do
283
+ caturday = "caturday"
284
+
285
+ ss = Session.new(host: HOST)
286
+
287
+ # Find the latest 100 toots. This assumes an average distribution
288
+ # that may not happen on the actual live server, in which case
289
+ # this test will fail for no good reason.
290
+ default_tl = ss.each_tag_status(caturday, limit: 200).to_a
291
+
292
+ # Some toots are federated
293
+ expect( default_tl.select{|st| st.account.acct.include? '@'}.empty? )
294
+ .to be false
295
+
296
+ # Some toots are local
297
+ expect( default_tl.reject{|st| st.account.acct.include? '@'}.empty? )
298
+ .to be false
299
+
300
+ # Some toots contain media
301
+ expect( default_tl.reject{|st| st.media_attachments.empty? }.empty? )
302
+ .to be false
303
+
304
+ # All statuses should contain media when selecting only_media
305
+ media_tl = ss.each_tag_status(caturday, limit: 20, only_media: true)
306
+ .to_a
307
+ expect( media_tl.select{|st| st.media_attachments.empty?} )
308
+ .to eq []
309
+
310
+ # local:true should return no toots from a remote site
311
+ local_tl = ss.each_tag_status(caturday, limit: 20, local: true).to_a;
312
+ expect( local_tl.select{|st| st.account.acct.include? '@' } )
313
+ .to eq []
314
+
315
+ # remote:true should return no toots from the local site
316
+ remote_tl = ss.each_tag_status("caturday", limit: 20, remote: true).to_a
317
+ expect( remote_tl.reject{|st| st.account.acct.include? '@' } )
318
+ .to eq []
319
+
320
+
321
+ #
322
+ # any/all/none
323
+ #
324
+
325
+ # Expect all of the extra tags
326
+ plus_all = ["cats", "catsofmastodon"]
327
+ remote_tl = ss.each_tag_status(caturday,
328
+ limit: 20,
329
+ all: plus_all
330
+ ).to_a
331
+ remote_tl.empty? and
332
+ puts "WARNING: found no posts with tags #{[caturday] + plus_all}"
333
+
334
+ remote_tl.each{|rstat|
335
+ found_tags = rstat.tags.map{|t| t.name.downcase}.to_set
336
+ expect( found_tags ) .to be >= plus_all.to_set
337
+ }
338
+
339
+
340
+ # Expect none of the extra tags
341
+ plus_none = ["cats", "catsofmastodon"]
342
+ remote_tl = ss.each_tag_status(caturday,
343
+ limit: 20,
344
+ none: plus_none
345
+ ).to_a
346
+ remote_tl.empty? and
347
+ puts"WARNING: found no posts w/ #{caturday} but not #{plus_none}"
348
+
349
+ remote_tl.each{|rstat|
350
+ found_tags = rstat.tags.map{|t| t.name.downcase}.to_set
351
+ expect( found_tags & plus_none ) .to be_empty
352
+ }
353
+
354
+ # Expect any of the following
355
+ plus_any = [caturday, "cats", "catsofmastodon"]
356
+ remote_tl = ss.each_tag_status(plus_any, limit: 20).to_a
357
+ remote_tl.empty? and
358
+ puts"WARNING: found no posts w/ #{caturday} but some of #{plus_any}"
359
+
360
+ expected = plus_any + [caturday]
361
+ remote_tl.each{|rstat|
362
+ found_tags = rstat.tags.map{|t| t.name.downcase}.to_set
363
+ expect( found_tags & expected ) .to_not be_empty
364
+ }
365
+ end
366
+ end
@@ -0,0 +1,96 @@
1
+
2
+ # Tests requiring authentication
3
+
4
+ require_relative 'spec_helper'
5
+ require_relative '../lib/shep'
6
+
7
+ include Shep
8
+
9
+ describe Session, :account do
10
+ it "fetches status sources." do
11
+ ss = Session.new(host: HOST, token: TOKEN)
12
+ statuses = ss.each_status(WRITE_ACCOUNT, limit: 1).to_a
13
+ expect( statuses.empty? ) .to be false
14
+ stat0 = statuses[0]
15
+
16
+ src = ss.fetch_status_src(stat0.id)
17
+
18
+ words = src.text.split
19
+ puts "Couldn't find a word in Entity::StatusSource" if words.empty?
20
+ expect( words.empty? || stat0.content.include?(words[0]))
21
+ .to be true
22
+ end
23
+
24
+ it "reads the home timeline" do
25
+ ss = Session.new(host: HOST, token: TOKEN)
26
+ statuses = ss.each_home_status(limit: 10).to_a
27
+ expect( statuses.size ) .to be > 0
28
+ end
29
+
30
+ it "reads notifications" do
31
+ ss = Session.new(host: HOST, token: TOKEN)
32
+
33
+ ntfns = ss.each_notification(limit: 10).to_a
34
+ expect( ntfns.size ) .to be > 0
35
+
36
+ ntfns.each{|ntfn|
37
+ ntfn2 = ss.fetch_notification(ntfn.id)
38
+ expect( ntfn2 ) .to eq ntfn
39
+ }
40
+ first = ntfns[0]
41
+
42
+ # Test 'types' filter
43
+ proc {
44
+ ntfns2 = ss
45
+ .each_notification(limit: 10, types: [first.type])
46
+ .to_a
47
+
48
+ expect( ntfns2[0] ) .to eq first
49
+
50
+ expect( ntfns2.select{|n| n.type == first.type} ) .to eq ntfns2
51
+ }.call
52
+
53
+ # Test 'exclude_types' filter
54
+ proc {
55
+ ntfns2 = ss
56
+ .each_notification(limit: 10, exclude_types: [first.type])
57
+ .to_a
58
+ expect( ntfns2[0] ) .to_not eq first
59
+ expect( ntfns2.map{|n| n.type}.include?(first.type) ) .to be false
60
+ }.call
61
+
62
+ # Test 'account_id' filter
63
+ proc {
64
+ ntfns2 = ss
65
+ .each_notification(limit: 10, account_id: first.account.id)
66
+ .to_a
67
+
68
+ expect( ntfns2[0] ) .to eq first
69
+
70
+ expect( ntfns2.select{|n| n.account.id == first.account.id} )
71
+ .to eq ntfns2
72
+ }.call
73
+ end
74
+
75
+ skip "dismisses notifications" do
76
+ ss = Session.new(host: HOST, token: TOKEN)
77
+
78
+ # We dismiss the first notification, then confirm that it is no
79
+ # longer there. Since this is not repeatable, we skip it by
80
+ # default and manually enable it when we want to test it.
81
+
82
+ ntfns = ss.each_notification(limit: 10).to_a
83
+ expect( ntfns.size ) .to be > 0
84
+ first = ntfns[0]
85
+
86
+ ss.dismiss_notification(first.id)
87
+
88
+ ntfns2 = ss.each_notification(limit: 10).to_a
89
+ expect( ntfns2.size ) .to eq ntfns.size - 1
90
+ expect( ntfns2[0] ) .to_not eq first
91
+ end
92
+
93
+
94
+ # skip "" do ; end
95
+
96
+ end