A simple Ruby server using Sinatra that serves Bluesky custom feeds
1require 'spec_helper'
2
3describe BlueFactory::OutputGenerator do
4 subject { BlueFactory::OutputGenerator.new }
5
6 let(:post_uri) { 'at://did:plc:abc123/app.bsky.feed.post/def456' }
7 let(:second_post_uri) { 'at://did:plc:abc123/app.bsky.feed.post/ghi789' }
8
9 it 'should generate feed output from received get_posts response' do
10 response = {
11 posts: [
12 {
13 :post => "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3lwtzoigbp22x",
14 :reason => { :pin => true }
15 },
16 "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f",
17 "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
18 {
19 :post => "at://did:plc:4adlzwqtkv4dirxjwq4c3tlm/app.bsky.feed.post/3mcvrvtlk2j2t",
20 :reason => { :repost => "at://did:plc:vmt7o7y6titkqzzxav247zrn/app.bsky.feed.repost/3md54l2q7zc2w" },
21 :context => "17480484288:28"
22 },
23 "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h",
24 {
25 :post => "at://did:plc:2zziubqb5v7bdw2ahteej7wr/app.bsky.feed.post/3md4wfrap6t2c"
26 }
27 ],
28 cursor: "2026-01-24T00:32:27.137Z",
29 req_id: "17480484288"
30 }
31
32 output = subject.generate(response)
33
34 output.should == {
35 feed: [
36 {
37 :post => "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3lwtzoigbp22x",
38 :reason => { "$type" => "app.bsky.feed.defs#skeletonReasonPin" }
39 },
40 { :post => "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f" },
41 { :post => "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r" },
42 {
43 :post => "at://did:plc:4adlzwqtkv4dirxjwq4c3tlm/app.bsky.feed.post/3mcvrvtlk2j2t",
44 :reason => {
45 "$type" => "app.bsky.feed.defs#skeletonReasonRepost",
46 "repost" => "at://did:plc:vmt7o7y6titkqzzxav247zrn/app.bsky.feed.repost/3md54l2q7zc2w"
47 },
48 :feedContext => "17480484288:28"
49 },
50 { :post => "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h" },
51 { :post => "at://did:plc:2zziubqb5v7bdw2ahteej7wr/app.bsky.feed.post/3md4wfrap6t2c" }
52 ],
53 cursor: "2026-01-24T00:32:27.137Z",
54 reqId: "17480484288"
55 }
56 end
57
58 it 'should not raise an error when cursor or req_id are not set' do
59 response = {
60 posts: [
61 "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f",
62 "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
63 "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h",
64 ]
65 }
66
67 output = subject.generate(response)
68
69 output.should == {
70 feed: [
71 { :post => "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f" },
72 { :post => "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r" },
73 { :post => "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h" },
74 ]
75 }
76 end
77
78 it 'should raise an error when posts field is missing' do
79 expect { subject.generate({}) }.to raise_error(BlueFactory::InvalidResponseError)
80 end
81
82 it 'should raise an error when posts field is not an array' do
83 response = "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f"
84
85 expect { subject.generate(posts: response) }.to raise_error(BlueFactory::InvalidResponseError)
86 end
87
88 it 'should convert cursor to a string' do
89 response = {
90 posts: [
91 "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f",
92 "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
93 "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h",
94 ],
95 cursor: 123
96 }
97
98 output = subject.generate(response)
99
100 output[:cursor].should == '123'
101 end
102
103 it 'should convert req_id to a string' do
104 response = {
105 posts: [
106 "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f",
107 "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
108 "at://did:plc:hpv2yni36g2b4ymwsdg2uwre/app.bsky.feed.post/3md4xjrcmj22h",
109 ],
110 req_id: 1024
111 }
112
113 output = subject.generate(response)
114
115 output[:reqId].should == '1024'
116 end
117
118 it 'should convert post context to a string' do
119 response = {
120 posts: [
121 "at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/app.bsky.feed.post/3md4khzlybs2f",
122 {
123 :post => "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
124 :context => 16777216
125 }
126 ]
127 }
128
129 output = subject.generate(response)
130
131 output[:feed][1][:feedContext].should == '16777216'
132 end
133
134 it 'should raise when a post string has an invalid URI' do
135 bad_posts = [
136 'bad uri',
137 :test,
138 [1, 2, 3],
139 { post: 'bad uri' },
140 { post: :test },
141 ]
142
143 bad_posts.each do |post|
144 expect { subject.generate(posts: [post]) }.to raise_error(BlueFactory::InvalidResponseError)
145 end
146 end
147
148 it 'should raise an error when a post hash is missing :post' do
149 response = {
150 posts: [
151 { reason: { pin: true }}
152 ]
153 }
154
155 expect { subject.generate(response) }.to raise_error(BlueFactory::InvalidResponseError)
156 end
157
158 it 'should raise an error when a post reason is not a hash' do
159 response = {
160 posts: [
161 {
162 post: "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
163 reason: 'test'
164 }
165 ]
166 }
167
168 expect { subject.generate(response) }.to raise_error(BlueFactory::InvalidResponseError)
169 end
170
171 it 'should raise an error when a post reason is not recognized' do
172 response = {
173 posts: [
174 {
175 post: "at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.post/3md5h3jg6as2r",
176 reason: { :test => true }
177 }
178 ]
179 }
180
181 expect { subject.generate(response) }.to raise_error(BlueFactory::InvalidResponseError)
182 end
183end