relay filter/appview bootstrap
1mod common;
2
3use chrono::{Duration, Utc};
4use reqwest::StatusCode;
5use serde_json::{json, Value};
6use prism::records::{NewAccount, NewRecord, CHANNEL_COLLECTION};
7use std::sync::atomic::{AtomicU64, Ordering};
8
9static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
10
11fn unique_suffix() -> String {
12 let ts = std::time::SystemTime::now()
13 .duration_since(std::time::UNIX_EPOCH)
14 .unwrap()
15 .as_nanos();
16 let count = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
17 format!("{}_{}", ts, count)
18}
19
20async fn insert_test_account(pool: &sqlx::PgPool, did: &str) {
21 prism::db::upsert_account(
22 pool,
23 &NewAccount {
24 did: did.to_string(),
25 handle: did.to_string(),
26 created_at: Utc::now(),
27 },
28 )
29 .await
30 .unwrap();
31}
32
33async fn insert_channel_at_time(
34 pool: &sqlx::PgPool,
35 cid: &str,
36 creator_did: &str,
37 name: &str,
38 indexed_at: chrono::DateTime<Utc>,
39) {
40 let uri = format!("at://{}/systems.gmstn.development.channel/{}", creator_did, cid);
41 prism::db::insert_record(
42 pool,
43 &NewRecord {
44 uri,
45 cid: cid.to_string(),
46 collection: CHANNEL_COLLECTION.to_string(),
47 creator_did: creator_did.to_string(),
48 created_at: indexed_at,
49 indexed_at,
50 data: json!({"name": name, "topic": "test topic"}),
51 target_did: None,
52 ref_cids: vec![],
53 },
54 )
55 .await
56 .unwrap();
57}
58
59#[tokio::test]
60async fn test_pagination_first_page_returns_latest() {
61 let base_url = common::base_url().await;
62 let pool = common::get_db_pool().await;
63 let client = common::client();
64
65 let suffix = unique_suffix();
66 let test_did = format!("did:plc:pagination_first_{}", suffix);
67 insert_test_account(&pool, &test_did).await;
68
69 let now = Utc::now();
70 insert_channel_at_time(&pool, &format!("bafypagfirst001_{}", suffix), &test_did, "Oldest", now - Duration::hours(3))
71 .await;
72 insert_channel_at_time(&pool, &format!("bafypagfirst002_{}", suffix), &test_did, "Middle", now - Duration::hours(2))
73 .await;
74 insert_channel_at_time(&pool, &format!("bafypagfirst003_{}", suffix), &test_did, "Newest", now - Duration::hours(1))
75 .await;
76
77 let res = client
78 .get(format!(
79 "{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=2",
80 base_url, test_did
81 ))
82 .send()
83 .await
84 .unwrap();
85
86 assert_eq!(res.status(), StatusCode::OK);
87
88 let body: Value = res.json().await.unwrap();
89 let channels = body["channels"].as_array().unwrap();
90
91 assert_eq!(channels.len(), 2);
92 assert_eq!(channels[0]["displayName"].as_str().unwrap(), "Newest");
93 assert_eq!(channels[1]["displayName"].as_str().unwrap(), "Middle");
94 assert!(body["cursor"].is_string());
95}
96
97#[tokio::test]
98async fn test_pagination_with_cursor_excludes_newer() {
99 let base_url = common::base_url().await;
100 let pool = common::get_db_pool().await;
101 let client = common::client();
102
103 let suffix = unique_suffix();
104 let test_did = format!("did:plc:pagination_cursor_{}", suffix);
105 insert_test_account(&pool, &test_did).await;
106
107 let now = Utc::now();
108 let oldest_time = now - Duration::hours(3);
109 let middle_time = now - Duration::hours(2);
110 let newest_time = now - Duration::hours(1);
111
112 insert_channel_at_time(&pool, &format!("bafypagcur001_{}", suffix), &test_did, "Oldest", oldest_time).await;
113 insert_channel_at_time(&pool, &format!("bafypagcur002_{}", suffix), &test_did, "Middle", middle_time).await;
114 insert_channel_at_time(&pool, &format!("bafypagcur003_{}", suffix), &test_did, "Newest", newest_time).await;
115
116 let cursor = middle_time.to_rfc3339();
117
118 let res = client
119 .get(format!(
120 "{}/xrpc/systems.gmstn.development.channel.listChannels",
121 base_url
122 ))
123 .query(&[("author", test_did.as_str()), ("cursor", cursor.as_str()), ("limit", "10")])
124 .send()
125 .await
126 .unwrap();
127
128 assert_eq!(res.status(), StatusCode::OK);
129
130 let body: Value = res.json().await.unwrap();
131 let channels = body["channels"].as_array().unwrap();
132
133 assert_eq!(channels.len(), 1);
134 assert_eq!(channels[0]["displayName"].as_str().unwrap(), "Oldest");
135}
136
137#[tokio::test]
138async fn test_pagination_returns_cursor_for_continuation() {
139 let base_url = common::base_url().await;
140 let pool = common::get_db_pool().await;
141 let client = common::client();
142
143 let suffix = unique_suffix();
144 let test_did = format!("did:plc:pagination_single_{}", suffix);
145 insert_test_account(&pool, &test_did).await;
146
147 let now = Utc::now();
148 insert_channel_at_time(&pool, &format!("bafypagsingle001_{}", suffix), &test_did, "Only", now - Duration::hours(1))
149 .await;
150
151 let res = client
152 .get(format!(
153 "{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=10",
154 base_url, test_did
155 ))
156 .send()
157 .await
158 .unwrap();
159
160 assert_eq!(res.status(), StatusCode::OK);
161
162 let body: Value = res.json().await.unwrap();
163 let channels = body["channels"].as_array().unwrap();
164
165 assert_eq!(channels.len(), 1);
166 assert!(body["cursor"].is_string());
167}
168
169#[tokio::test]
170async fn test_pagination_full_traversal() {
171 let base_url = common::base_url().await;
172 let pool = common::get_db_pool().await;
173 let client = common::client();
174
175 let suffix = unique_suffix();
176 let test_did = format!("did:plc:pagination_full_{}", suffix);
177 insert_test_account(&pool, &test_did).await;
178
179 let now = Utc::now();
180 for i in 0..5 {
181 insert_channel_at_time(
182 &pool,
183 &format!("bafypagfull{:03}_{}", i, suffix),
184 &test_did,
185 &format!("Channel {}", i),
186 now - Duration::hours(5 - i as i64),
187 )
188 .await;
189 }
190
191 let mut all_names: Vec<String> = Vec::new();
192 let mut cursor: Option<String> = None;
193
194 loop {
195 let mut req = client
196 .get(format!(
197 "{}/xrpc/systems.gmstn.development.channel.listChannels",
198 base_url
199 ))
200 .query(&[("author", test_did.as_str()), ("limit", "2")]);
201
202 if let Some(c) = &cursor {
203 req = req.query(&[("cursor", c.as_str())]);
204 }
205
206 let res = req.send().await.unwrap();
207 assert_eq!(res.status(), StatusCode::OK);
208
209 let body: Value = res.json().await.unwrap();
210 let channels = body["channels"].as_array().unwrap();
211
212 if channels.is_empty() {
213 break;
214 }
215
216 for channel in channels {
217 all_names.push(channel["displayName"].as_str().unwrap().to_string());
218 }
219
220 cursor = body["cursor"].as_str().map(String::from);
221
222 if cursor.is_none() {
223 break;
224 }
225 }
226
227 assert_eq!(all_names.len(), 5);
228 assert_eq!(all_names[0], "Channel 4");
229 assert_eq!(all_names[4], "Channel 0");
230}