Rust AppView - highly experimental!
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: update lexica

+1305 -5
+25 -5
lexica/src/app_bsky/feed.rs
··· 44 44 pub indexed_at: DateTime<Utc>, 45 45 } 46 46 47 - #[derive(Debug, Serialize, Deserialize)] 47 + #[derive(Clone, Debug, Serialize, Deserialize)] 48 48 #[serde(rename_all = "camelCase")] 49 49 pub struct FeedViewPost { 50 50 pub post: PostView, ··· 56 56 pub feed_context: Option<String>, 57 57 } 58 58 59 - #[derive(Debug, Serialize, Deserialize)] 59 + #[derive(Clone, Debug, Serialize, Deserialize)] 60 60 #[serde(rename_all = "camelCase")] 61 61 pub struct ReplyRef { 62 62 pub root: ReplyRefPost, ··· 65 65 pub grandparent_author: Option<ProfileViewBasic>, 66 66 } 67 67 68 - #[derive(Debug, Serialize, Deserialize)] 68 + #[derive(Clone, Debug, Serialize, Deserialize)] 69 69 #[serde(tag = "$type")] 70 70 pub enum ReplyRefPost { 71 71 #[serde(rename = "app.bsky.feed.defs#postView")] ··· 84 84 }, 85 85 } 86 86 87 - #[derive(Debug, Serialize, Deserialize)] 87 + #[derive(Clone, Debug, Serialize, Deserialize)] 88 88 #[serde(tag = "$type")] 89 89 pub enum FeedViewPostReason { 90 90 #[serde(rename = "app.bsky.feed.defs#reasonRepost")] ··· 93 93 Pin, 94 94 } 95 95 96 - #[derive(Debug, Serialize, Deserialize)] 96 + #[derive(Clone, Debug, Serialize, Deserialize)] 97 97 #[serde(rename_all = "camelCase")] 98 98 pub struct FeedReasonRepost { 99 99 pub by: ProfileViewBasic, ··· 298 298 pub struct SendInteractionsRequest { 299 299 pub interactions: Vec<Interaction>, 300 300 } 301 + 302 + // ============================================================================ 303 + // Request/Response Types for Feed Endpoints 304 + // ============================================================================ 305 + 306 + /// Response from getPosts 307 + #[derive(Clone, Debug, Deserialize, Serialize)] 308 + #[serde(rename_all = "camelCase")] 309 + pub struct GetPostsResponse { 310 + pub posts: Vec<PostView>, 311 + } 312 + 313 + /// Response from getAuthorFeed 314 + #[derive(Clone, Debug, Deserialize, Serialize)] 315 + #[serde(rename_all = "camelCase")] 316 + pub struct GetAuthorFeedResponse { 317 + pub feed: Vec<FeedViewPost>, 318 + #[serde(skip_serializing_if = "Option::is_none")] 319 + pub cursor: Option<String>, 320 + }
+322
lexica/tests/actor_types_test.rs
··· 1 + //! Tests for app.bsky.actor types against actual API responses 2 + //! 3 + //! These tests verify that our types can correctly deserialize 4 + //! real responses from the Bluesky API. 5 + 6 + use lexica::app_bsky::actor::*; 7 + use serde_json; 8 + 9 + #[test] 10 + fn test_profile_view_detailed_deserialization() { 11 + // This is a real response from the Bluesky API (with some fields simplified) 12 + let json = r#"{ 13 + "did": "did:plc:jrtgsidnmxaen4offglr5lsh", 14 + "handle": "quilling.dev", 15 + "displayName": "teq", 16 + "description": "torment nexus dismantler", 17 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:jrtgsidnmxaen4offglr5lsh/bafkreigwqzlvyzspam75vsrenyew265cud6qw4kbeuvpwndl2qr3myb6ye@jpeg", 18 + "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:jrtgsidnmxaen4offglr5lsh/bafkreib2eya5gp5uqycbxefbugkzfgz2qwgs26fi7htpexc2krdsv2dwqe@jpeg", 19 + "followersCount": 1527, 20 + "followsCount": 417, 21 + "postsCount": 6142, 22 + "associated": { 23 + "lists": 0, 24 + "feedgens": 1, 25 + "starterPacks": 0, 26 + "labeler": false, 27 + "chat": { 28 + "allowIncoming": "all" 29 + }, 30 + "activitySubscription": { 31 + "allowSubscriptions": "followers" 32 + } 33 + }, 34 + "labels": [], 35 + "createdAt": "2023-12-08T15:37:09.656Z", 36 + "indexedAt": "2025-11-29T03:29:57.825Z", 37 + "pinnedPost": { 38 + "cid": "bafyreie4lqcc65pp7f62eix5byyjwb5y5z2pookouff2d4zdwfb7ub4tu4", 39 + "uri": "at://did:plc:jrtgsidnmxaen4offglr5lsh/app.bsky.feed.post/3lx6gs6lq5k2y" 40 + } 41 + }"#; 42 + 43 + let profile: ProfileViewDetailed = serde_json::from_str(json).expect("Failed to deserialize ProfileViewDetailed"); 44 + 45 + assert_eq!(profile.did, "did:plc:jrtgsidnmxaen4offglr5lsh"); 46 + assert_eq!(profile.handle, "quilling.dev"); 47 + assert_eq!(profile.display_name.as_deref(), Some("teq")); 48 + assert_eq!(profile.followers_count, 1527); 49 + assert_eq!(profile.follows_count, 417); 50 + assert_eq!(profile.posts_count, 6142); 51 + 52 + // Check associated fields 53 + let associated = profile.associated.expect("Should have associated data"); 54 + assert_eq!(associated.lists, Some(0)); 55 + assert_eq!(associated.feedgens, Some(1)); 56 + assert_eq!(associated.labeler, Some(false)); 57 + 58 + // Check pinned post 59 + let pinned = profile.pinned_post.expect("Should have pinned post"); 60 + assert_eq!(pinned.cid.to_string(), "bafyreie4lqcc65pp7f62eix5byyjwb5y5z2pookouff2d4zdwfb7ub4tu4"); 61 + assert_eq!(pinned.uri, "at://did:plc:jrtgsidnmxaen4offglr5lsh/app.bsky.feed.post/3lx6gs6lq5k2y"); 62 + } 63 + 64 + #[test] 65 + fn test_get_profiles_response_deserialization() { 66 + let json = r#"{ 67 + "profiles": [ 68 + { 69 + "did": "did:plc:test1", 70 + "handle": "test1.bsky.social", 71 + "followersCount": 100, 72 + "followsCount": 50, 73 + "postsCount": 200, 74 + "createdAt": "2023-01-01T00:00:00Z", 75 + "indexedAt": "2023-01-01T00:00:00Z", 76 + "labels": [] 77 + }, 78 + { 79 + "did": "did:plc:test2", 80 + "handle": "test2.bsky.social", 81 + "displayName": "Test User 2", 82 + "followersCount": 200, 83 + "followsCount": 100, 84 + "postsCount": 500, 85 + "createdAt": "2023-01-01T00:00:00Z", 86 + "indexedAt": "2023-01-01T00:00:00Z", 87 + "labels": [] 88 + } 89 + ] 90 + }"#; 91 + 92 + let response: GetProfilesResponse = serde_json::from_str(json) 93 + .expect("Failed to deserialize GetProfilesResponse"); 94 + 95 + assert_eq!(response.profiles.len(), 2); 96 + assert_eq!(response.profiles[0].did, "did:plc:test1"); 97 + assert_eq!(response.profiles[0].handle, "test1.bsky.social"); 98 + assert_eq!(response.profiles[1].did, "did:plc:test2"); 99 + assert_eq!(response.profiles[1].display_name.as_deref(), Some("Test User 2")); 100 + } 101 + 102 + #[test] 103 + fn test_search_actors_response_with_cursor() { 104 + let json = r#"{ 105 + "actors": [ 106 + { 107 + "did": "did:plc:test", 108 + "handle": "test.bsky.social", 109 + "createdAt": "2023-01-01T00:00:00Z", 110 + "indexedAt": "2023-01-01T00:00:00Z", 111 + "labels": [] 112 + } 113 + ], 114 + "cursor": "123456" 115 + }"#; 116 + 117 + let response: SearchActorsResponse = serde_json::from_str(json) 118 + .expect("Failed to deserialize SearchActorsResponse"); 119 + 120 + assert_eq!(response.actors.len(), 1); 121 + assert_eq!(response.cursor, Some("123456".to_string())); 122 + } 123 + 124 + #[test] 125 + fn test_search_actors_response_without_cursor() { 126 + let json = r#"{ 127 + "actors": [] 128 + }"#; 129 + 130 + let response: SearchActorsResponse = serde_json::from_str(json) 131 + .expect("Failed to deserialize SearchActorsResponse"); 132 + 133 + assert_eq!(response.actors.len(), 0); 134 + assert_eq!(response.cursor, None); 135 + } 136 + 137 + #[test] 138 + fn test_known_followers_deserialization() { 139 + let json = r#"{ 140 + "count": 5, 141 + "followers": [ 142 + { 143 + "did": "did:plc:follower1", 144 + "handle": "follower1.bsky.social", 145 + "createdAt": "2023-01-01T00:00:00Z", 146 + "labels": [] 147 + }, 148 + { 149 + "did": "did:plc:follower2", 150 + "handle": "follower2.bsky.social", 151 + "displayName": "Follower Two", 152 + "createdAt": "2023-01-01T00:00:00Z", 153 + "labels": [] 154 + } 155 + ] 156 + }"#; 157 + 158 + let known_followers: KnownFollowers = serde_json::from_str(json) 159 + .expect("Failed to deserialize KnownFollowers"); 160 + 161 + assert_eq!(known_followers.count, 5); 162 + assert_eq!(known_followers.followers.len(), 2); 163 + assert_eq!(known_followers.followers[0].did, "did:plc:follower1"); 164 + assert_eq!(known_followers.followers[1].display_name.as_deref(), Some("Follower Two")); 165 + } 166 + 167 + #[test] 168 + fn test_profile_viewer_state_with_known_followers() { 169 + let json = r#"{ 170 + "muted": false, 171 + "blockedBy": false, 172 + "following": "at://did:plc:test/app.bsky.graph.follow/123", 173 + "knownFollowers": { 174 + "count": 2, 175 + "followers": [ 176 + { 177 + "did": "did:plc:mutual", 178 + "handle": "mutual.bsky.social", 179 + "createdAt": "2023-01-01T00:00:00Z", 180 + "labels": [] 181 + } 182 + ] 183 + } 184 + }"#; 185 + 186 + let viewer_state: ProfileViewerState = serde_json::from_str(json) 187 + .expect("Failed to deserialize ProfileViewerState with knownFollowers"); 188 + 189 + assert!(!viewer_state.muted); 190 + assert!(!viewer_state.blocked_by); 191 + assert_eq!(viewer_state.following.as_deref(), Some("at://did:plc:test/app.bsky.graph.follow/123")); 192 + 193 + let known_followers = viewer_state.known_followers.expect("Should have known followers"); 194 + assert_eq!(known_followers.count, 2); 195 + assert_eq!(known_followers.followers.len(), 1); 196 + } 197 + 198 + #[test] 199 + fn test_profile_associated_chat_types() { 200 + // Test "all" variant 201 + let json = r#"{"allowIncoming": "all"}"#; 202 + let chat: ProfileAssociatedChat = serde_json::from_str(json).unwrap(); 203 + assert!(matches!(chat.allow_incoming, ChatAllowIncoming::All)); 204 + 205 + // Test "none" variant 206 + let json = r#"{"allowIncoming": "none"}"#; 207 + let chat: ProfileAssociatedChat = serde_json::from_str(json).unwrap(); 208 + assert!(matches!(chat.allow_incoming, ChatAllowIncoming::None)); 209 + 210 + // Test "following" variant 211 + let json = r#"{"allowIncoming": "following"}"#; 212 + let chat: ProfileAssociatedChat = serde_json::from_str(json).unwrap(); 213 + assert!(matches!(chat.allow_incoming, ChatAllowIncoming::Following)); 214 + } 215 + 216 + #[test] 217 + fn test_request_params_deserialization() { 218 + // Test GetProfilesParams 219 + let json = r#"{"actors": ["did:plc:test1", "did:plc:test2"]}"#; 220 + let params: GetProfilesParams = serde_json::from_str(json).unwrap(); 221 + assert_eq!(params.actors.len(), 2); 222 + 223 + // Test SearchActorsParams with all fields 224 + let json = r#"{"q": "search query", "limit": 50, "cursor": "next"}"#; 225 + let params: SearchActorsParams = serde_json::from_str(json).unwrap(); 226 + assert_eq!(params.q.as_deref(), Some("search query")); 227 + assert_eq!(params.limit, Some(50)); 228 + assert_eq!(params.cursor.as_deref(), Some("next")); 229 + 230 + // Test SearchActorsParams with deprecated term field 231 + let json = r#"{"term": "old query"}"#; 232 + let params: SearchActorsParams = serde_json::from_str(json).unwrap(); 233 + assert_eq!(params.term.as_deref(), Some("old query")); 234 + assert_eq!(params.q, None); 235 + } 236 + 237 + #[test] 238 + fn test_profile_with_joined_via_starter_pack() { 239 + // This tests that joinedViaStarterPack can be deserialized when present 240 + let json = r#"{ 241 + "did": "did:plc:test", 242 + "handle": "test.bsky.social", 243 + "followersCount": 0, 244 + "followsCount": 0, 245 + "postsCount": 0, 246 + "joinedViaStarterPack": { 247 + "uri": "at://did:plc:creator/app.bsky.graph.starterpack/123", 248 + "cid": "bafyreie4lqcc65pp7f62eix5byyjwb5y5z2pookouff2d4zdwfb7ub4tu4", 249 + "record": {}, 250 + "creator": { 251 + "did": "did:plc:creator", 252 + "handle": "creator.bsky.social", 253 + "createdAt": "2023-01-01T00:00:00Z", 254 + "labels": [] 255 + }, 256 + "listItemCount": 10, 257 + "joinedWeekCount": 5, 258 + "joinedAllTimeCount": 100, 259 + "labels": [], 260 + "indexedAt": "2023-01-01T00:00:00Z" 261 + }, 262 + "createdAt": "2023-01-01T00:00:00Z", 263 + "indexedAt": "2023-01-01T00:00:00Z", 264 + "labels": [] 265 + }"#; 266 + 267 + let profile: ProfileViewDetailed = serde_json::from_str(json) 268 + .expect("Failed to deserialize ProfileViewDetailed with joinedViaStarterPack"); 269 + 270 + let starter_pack = profile.joined_via_starter_pack.expect("Should have starter pack"); 271 + assert_eq!(starter_pack.uri, "at://did:plc:creator/app.bsky.graph.starterpack/123"); 272 + assert_eq!(starter_pack.list_item_count, 10); 273 + assert_eq!(starter_pack.joined_week_count, 5); 274 + assert_eq!(starter_pack.joined_all_time_count, 100); 275 + } 276 + 277 + #[test] 278 + fn test_serialization_roundtrip() { 279 + // Create a ProfileViewDetailed programmatically 280 + let profile = ProfileViewDetailed { 281 + did: "did:plc:test".to_string(), 282 + handle: "test.bsky.social".to_string(), 283 + display_name: Some("Test User".to_string()), 284 + description: Some("A test profile".to_string()), 285 + avatar: None, 286 + banner: None, 287 + website: Some("https://example.com".to_string()), 288 + pronouns: Some("they/them".to_string()), 289 + followers_count: 100, 290 + follows_count: 50, 291 + posts_count: 200, 292 + associated: Some(ProfileAssociated { 293 + lists: Some(1), 294 + feedgens: Some(2), 295 + starter_packs: Some(0), 296 + labeler: Some(false), 297 + chat: None, 298 + activity_subscription: None, 299 + }), 300 + joined_via_starter_pack: None, 301 + viewer: None, 302 + labels: vec![], 303 + pinned_post: None, 304 + verification: None, 305 + status: None, 306 + created_at: chrono::Utc::now(), 307 + indexed_at: chrono::Utc::now(), 308 + }; 309 + 310 + // Serialize to JSON 311 + let json = serde_json::to_string(&profile).expect("Failed to serialize"); 312 + 313 + // Deserialize back 314 + let deserialized: ProfileViewDetailed = serde_json::from_str(&json) 315 + .expect("Failed to deserialize"); 316 + 317 + // Check key fields match 318 + assert_eq!(deserialized.did, profile.did); 319 + assert_eq!(deserialized.handle, profile.handle); 320 + assert_eq!(deserialized.display_name, profile.display_name); 321 + assert_eq!(deserialized.followers_count, profile.followers_count); 322 + }
+525
lexica/tests/appview_endpoints_test.rs
··· 1 + //! Tests for AppView endpoint types to ensure they match real API responses 2 + //! 3 + //! These tests verify that our types can correctly deserialize actual responses 4 + //! from Bluesky's AppView endpoints that parakeet implements. 5 + 6 + use lexica::app_bsky::actor::*; 7 + use lexica::app_bsky::feed::*; 8 + use lexica::app_bsky::graph::*; 9 + use lexica::app_bsky::notification::*; 10 + use serde_json; 11 + 12 + // ============================================================================ 13 + // Actor Endpoint Tests 14 + // ============================================================================ 15 + 16 + #[test] 17 + fn test_get_profiles_request() { 18 + // Test that GetProfilesParams can deserialize actual request parameters 19 + let json = r#"{ 20 + "actors": ["did:plc:test1", "did:plc:test2", "did:plc:test3"] 21 + }"#; 22 + 23 + let params: GetProfilesParams = serde_json::from_str(json) 24 + .expect("Failed to deserialize GetProfilesParams"); 25 + 26 + assert_eq!(params.actors.len(), 3); 27 + assert_eq!(params.actors[0], "did:plc:test1"); 28 + } 29 + 30 + #[test] 31 + fn test_search_actors_params() { 32 + let json = r#"{ 33 + "q": "alice", 34 + "limit": 25, 35 + "cursor": "1234567890" 36 + }"#; 37 + 38 + let params: SearchActorsParams = serde_json::from_str(json) 39 + .expect("Failed to deserialize SearchActorsParams"); 40 + 41 + assert_eq!(params.q, Some("alice".to_string())); 42 + assert_eq!(params.limit, Some(25)); 43 + assert_eq!(params.cursor, Some("1234567890".to_string())); 44 + } 45 + 46 + #[test] 47 + fn test_search_actors_response() { 48 + let json = r#"{ 49 + "actors": [ 50 + { 51 + "did": "did:plc:test", 52 + "handle": "test.bsky.social", 53 + "displayName": "Test User", 54 + "createdAt": "2023-01-01T00:00:00Z", 55 + "indexedAt": "2023-01-01T00:00:00Z", 56 + "labels": [] 57 + } 58 + ], 59 + "cursor": "next_page" 60 + }"#; 61 + 62 + let response: SearchActorsResponse = serde_json::from_str(json) 63 + .expect("Failed to deserialize SearchActorsResponse"); 64 + 65 + assert_eq!(response.actors.len(), 1); 66 + assert_eq!(response.cursor, Some("next_page".to_string())); 67 + } 68 + 69 + // ============================================================================ 70 + // Feed Endpoint Tests 71 + // ============================================================================ 72 + 73 + #[test] 74 + fn test_feed_interaction() { 75 + let json = r#"{ 76 + "item": "at://did:plc:test/app.bsky.feed.post/123", 77 + "event": "app.bsky.feed.defs#interactionLike", 78 + "feedContext": "algorithm:trending" 79 + }"#; 80 + 81 + let interaction: Interaction = serde_json::from_str(json) 82 + .expect("Failed to deserialize Interaction"); 83 + 84 + assert_eq!(interaction.item, "at://did:plc:test/app.bsky.feed.post/123"); 85 + assert_eq!(interaction.event, InteractionEvent::InteractionLike); 86 + assert_eq!(interaction.feed_context, Some("algorithm:trending".to_string())); 87 + } 88 + 89 + #[test] 90 + fn test_send_interactions_request() { 91 + let json = r#"{ 92 + "interactions": [ 93 + { 94 + "item": "at://did:plc:test/app.bsky.feed.post/123", 95 + "event": "app.bsky.feed.defs#interactionSeen" 96 + }, 97 + { 98 + "item": "at://did:plc:test/app.bsky.feed.post/456", 99 + "event": "app.bsky.feed.defs#clickthroughAuthor", 100 + "feedContext": "following" 101 + } 102 + ] 103 + }"#; 104 + 105 + let request: SendInteractionsRequest = serde_json::from_str(json) 106 + .expect("Failed to deserialize SendInteractionsRequest"); 107 + 108 + assert_eq!(request.interactions.len(), 2); 109 + assert_eq!(request.interactions[0].event, InteractionEvent::InteractionSeen); 110 + assert_eq!(request.interactions[1].event, InteractionEvent::ClickthroughAuthor); 111 + } 112 + 113 + // ============================================================================ 114 + // Graph Endpoint Tests 115 + // ============================================================================ 116 + 117 + #[test] 118 + fn test_get_relationships_params() { 119 + let json = r#"{ 120 + "actor": "did:plc:main", 121 + "others": ["did:plc:other1", "did:plc:other2"] 122 + }"#; 123 + 124 + let params: GetRelationshipsParams = serde_json::from_str(json) 125 + .expect("Failed to deserialize GetRelationshipsParams"); 126 + 127 + assert_eq!(params.actor, "did:plc:main"); 128 + assert_eq!(params.others.as_ref().unwrap().len(), 2); 129 + } 130 + 131 + #[test] 132 + fn test_get_relationships_response() { 133 + let json = r#"{ 134 + "actor": "did:plc:main", 135 + "relationships": [ 136 + { 137 + "$type": "app.bsky.graph.defs#relationship", 138 + "did": "did:plc:other1", 139 + "following": "at://did:plc:main/app.bsky.graph.follow/123", 140 + "followedBy": "at://did:plc:other1/app.bsky.graph.follow/456" 141 + }, 142 + { 143 + "$type": "app.bsky.graph.defs#notFoundActor", 144 + "actor": "did:plc:other2", 145 + "notFound": true 146 + } 147 + ] 148 + }"#; 149 + 150 + let response: GetRelationshipsResponse = serde_json::from_str(json) 151 + .expect("Failed to deserialize GetRelationshipsResponse"); 152 + 153 + assert_eq!(response.actor, Some("did:plc:main".to_string())); 154 + assert_eq!(response.relationships.len(), 2); 155 + 156 + // Check the first relationship 157 + match &response.relationships[0] { 158 + RelationshipUnion::Relationship(rel) => { 159 + assert_eq!(rel.did, "did:plc:other1"); 160 + assert!(rel.following.is_some()); 161 + assert!(rel.followed_by.is_some()); 162 + } 163 + _ => panic!("Expected Relationship variant"), 164 + } 165 + 166 + // Check the not found actor 167 + match &response.relationships[1] { 168 + RelationshipUnion::NotFoundActor(not_found) => { 169 + assert_eq!(not_found.actor, "did:plc:other2"); 170 + assert!(not_found.not_found); 171 + } 172 + _ => panic!("Expected NotFoundActor variant"), 173 + } 174 + } 175 + 176 + #[test] 177 + fn test_get_follows_response() { 178 + let json = r#"{ 179 + "subject": { 180 + "did": "did:plc:subject", 181 + "handle": "subject.bsky.social", 182 + "createdAt": "2023-01-01T00:00:00Z", 183 + "indexedAt": "2023-01-01T00:00:00Z", 184 + "labels": [] 185 + }, 186 + "follows": [ 187 + { 188 + "did": "did:plc:follow1", 189 + "handle": "follow1.bsky.social", 190 + "createdAt": "2023-01-01T00:00:00Z", 191 + "indexedAt": "2023-01-01T00:00:00Z", 192 + "labels": [] 193 + } 194 + ], 195 + "cursor": "next" 196 + }"#; 197 + 198 + let response: GetFollowsResponse = serde_json::from_str(json) 199 + .expect("Failed to deserialize GetFollowsResponse"); 200 + 201 + assert_eq!(response.subject.did, "did:plc:subject"); 202 + assert_eq!(response.follows.len(), 1); 203 + assert_eq!(response.cursor, Some("next".to_string())); 204 + } 205 + 206 + #[test] 207 + fn test_get_list_response() { 208 + let json = r#"{ 209 + "list": { 210 + "uri": "at://did:plc:owner/app.bsky.graph.list/123", 211 + "cid": "bafyreie...", 212 + "name": "My List", 213 + "creator": { 214 + "did": "did:plc:owner", 215 + "handle": "owner.bsky.social", 216 + "createdAt": "2023-01-01T00:00:00Z", 217 + "indexedAt": "2023-01-01T00:00:00Z", 218 + "labels": [] 219 + }, 220 + "purpose": "app.bsky.graph.defs#curatelist", 221 + "listItemCount": 10, 222 + "labels": [], 223 + "indexedAt": "2023-01-01T00:00:00Z" 224 + }, 225 + "items": [ 226 + { 227 + "uri": "at://did:plc:owner/app.bsky.graph.listitem/456", 228 + "subject": { 229 + "did": "did:plc:member", 230 + "handle": "member.bsky.social", 231 + "createdAt": "2023-01-01T00:00:00Z", 232 + "indexedAt": "2023-01-01T00:00:00Z", 233 + "labels": [] 234 + } 235 + } 236 + ] 237 + }"#; 238 + 239 + let response: GetListResponse = serde_json::from_str(json) 240 + .expect("Failed to deserialize GetListResponse"); 241 + 242 + assert_eq!(response.list.name, "My List"); 243 + assert_eq!(response.list.list_item_count, 10); 244 + assert_eq!(response.items.len(), 1); 245 + } 246 + 247 + #[test] 248 + fn test_get_starter_pack_response() { 249 + let json = r#"{ 250 + "starterPack": { 251 + "uri": "at://did:plc:owner/app.bsky.graph.starterpack/123", 252 + "cid": "bafyreie...", 253 + "record": { 254 + "$type": "app.bsky.graph.starterpack", 255 + "name": "Cool People", 256 + "createdAt": "2023-01-01T00:00:00Z" 257 + }, 258 + "creator": { 259 + "did": "did:plc:owner", 260 + "handle": "owner.bsky.social", 261 + "createdAt": "2023-01-01T00:00:00Z", 262 + "labels": [] 263 + }, 264 + "listItemCount": 5, 265 + "joinedWeekCount": 10, 266 + "joinedAllTimeCount": 100, 267 + "labels": [], 268 + "indexedAt": "2023-01-01T00:00:00Z" 269 + } 270 + }"#; 271 + 272 + let response: GetStarterPackResponse = serde_json::from_str(json) 273 + .expect("Failed to deserialize GetStarterPackResponse"); 274 + 275 + assert_eq!(response.starter_pack.list_item_count, 5); 276 + assert_eq!(response.starter_pack.joined_week_count, 10); 277 + } 278 + 279 + // ============================================================================ 280 + // Notification Endpoint Tests 281 + // ============================================================================ 282 + 283 + #[test] 284 + fn test_notification_deserialization() { 285 + let json = r#"{ 286 + "uri": "at://did:plc:test/app.bsky.feed.like/123", 287 + "cid": "bafyreie...", 288 + "author": { 289 + "did": "did:plc:liker", 290 + "handle": "liker.bsky.social", 291 + "createdAt": "2023-01-01T00:00:00Z", 292 + "indexedAt": "2023-01-01T00:00:00Z", 293 + "labels": [] 294 + }, 295 + "reason": "like", 296 + "reasonSubject": "at://did:plc:user/app.bsky.feed.post/456", 297 + "record": { 298 + "$type": "app.bsky.feed.like", 299 + "subject": { 300 + "uri": "at://did:plc:user/app.bsky.feed.post/456", 301 + "cid": "bafyreie..." 302 + }, 303 + "createdAt": "2023-01-01T00:00:00Z" 304 + }, 305 + "isRead": false, 306 + "indexedAt": "2023-01-01T00:00:00Z", 307 + "labels": [] 308 + }"#; 309 + 310 + let notification: Notification = serde_json::from_str(json) 311 + .expect("Failed to deserialize Notification"); 312 + 313 + assert_eq!(notification.reason, NotificationReason::Like); 314 + assert!(!notification.is_read); 315 + assert!(notification.reason_subject.is_some()); 316 + } 317 + 318 + #[test] 319 + fn test_list_notifications_params() { 320 + let json = r#"{ 321 + "reasons": ["like", "follow", "mention"], 322 + "limit": 50, 323 + "priority": true, 324 + "cursor": "cursor123", 325 + "seenAt": "2023-01-01T00:00:00Z" 326 + }"#; 327 + 328 + let params: ListNotificationsParams = serde_json::from_str(json) 329 + .expect("Failed to deserialize ListNotificationsParams"); 330 + 331 + assert_eq!(params.reasons.as_ref().unwrap().len(), 3); 332 + assert_eq!(params.limit, Some(50)); 333 + assert_eq!(params.priority, Some(true)); 334 + } 335 + 336 + #[test] 337 + fn test_list_notifications_response() { 338 + let json = r#"{ 339 + "notifications": [ 340 + { 341 + "uri": "at://did:plc:test/app.bsky.graph.follow/123", 342 + "cid": "bafyreie...", 343 + "author": { 344 + "did": "did:plc:follower", 345 + "handle": "follower.bsky.social", 346 + "createdAt": "2023-01-01T00:00:00Z", 347 + "indexedAt": "2023-01-01T00:00:00Z", 348 + "labels": [] 349 + }, 350 + "reason": "follow", 351 + "record": { 352 + "$type": "app.bsky.graph.follow", 353 + "subject": "did:plc:user", 354 + "createdAt": "2023-01-01T00:00:00Z" 355 + }, 356 + "isRead": true, 357 + "indexedAt": "2023-01-01T00:00:00Z" 358 + } 359 + ], 360 + "cursor": "next", 361 + "priority": false, 362 + "seenAt": "2023-01-01T00:00:00Z" 363 + }"#; 364 + 365 + let response: ListNotificationsResponse = serde_json::from_str(json) 366 + .expect("Failed to deserialize ListNotificationsResponse"); 367 + 368 + assert_eq!(response.notifications.len(), 1); 369 + assert_eq!(response.notifications[0].reason, NotificationReason::Follow); 370 + assert!(response.notifications[0].is_read); 371 + assert_eq!(response.cursor, Some("next".to_string())); 372 + } 373 + 374 + #[test] 375 + fn test_get_unread_count_response() { 376 + let json = r#"{ 377 + "count": 42, 378 + "priorityCount": 5 379 + }"#; 380 + 381 + let response: GetUnreadCountResponse = serde_json::from_str(json) 382 + .expect("Failed to deserialize GetUnreadCountResponse"); 383 + 384 + assert_eq!(response.count, 42); 385 + assert_eq!(response.priority_count, Some(5)); 386 + } 387 + 388 + // ============================================================================ 389 + // Edge Cases and Complex Types 390 + // ============================================================================ 391 + 392 + #[test] 393 + fn test_profile_with_all_fields() { 394 + // Test ProfileViewDetailed with all optional fields populated 395 + let json = r#"{ 396 + "did": "did:plc:test", 397 + "handle": "test.bsky.social", 398 + "displayName": "Test User", 399 + "description": "Bio text", 400 + "avatar": "https://cdn.bsky.social/avatar/123", 401 + "banner": "https://cdn.bsky.social/banner/456", 402 + "followersCount": 1000, 403 + "followsCount": 500, 404 + "postsCount": 2000, 405 + "associated": { 406 + "lists": 5, 407 + "feedgens": 2, 408 + "starterPacks": 1, 409 + "labeler": true, 410 + "chat": { 411 + "allowIncoming": "following" 412 + }, 413 + "activitySubscription": { 414 + "allowSubscriptions": "followers" 415 + } 416 + }, 417 + "joinedViaStarterPack": { 418 + "uri": "at://did:plc:creator/app.bsky.graph.starterpack/789", 419 + "cid": "bafyreie...", 420 + "record": {}, 421 + "creator": { 422 + "did": "did:plc:creator", 423 + "handle": "creator.bsky.social", 424 + "createdAt": "2023-01-01T00:00:00Z", 425 + "labels": [] 426 + }, 427 + "listItemCount": 10, 428 + "joinedWeekCount": 5, 429 + "joinedAllTimeCount": 100, 430 + "labels": [], 431 + "indexedAt": "2023-01-01T00:00:00Z" 432 + }, 433 + "viewer": { 434 + "muted": false, 435 + "blockedBy": false, 436 + "following": "at://did:plc:viewer/app.bsky.graph.follow/abc", 437 + "followedBy": "at://did:plc:test/app.bsky.graph.follow/def", 438 + "knownFollowers": { 439 + "count": 3, 440 + "followers": [ 441 + { 442 + "did": "did:plc:mutual", 443 + "handle": "mutual.bsky.social", 444 + "createdAt": "2023-01-01T00:00:00Z", 445 + "labels": [] 446 + } 447 + ] 448 + } 449 + }, 450 + "labels": [], 451 + "pinnedPost": { 452 + "uri": "at://did:plc:test/app.bsky.feed.post/pinned123", 453 + "cid": "bafyreie..." 454 + }, 455 + "pronouns": "they/them", 456 + "website": "https://example.com", 457 + "createdAt": "2022-01-01T00:00:00Z", 458 + "indexedAt": "2023-12-01T00:00:00Z" 459 + }"#; 460 + 461 + let profile: ProfileViewDetailed = serde_json::from_str(json) 462 + .expect("Failed to deserialize ProfileViewDetailed with all fields"); 463 + 464 + // Verify key fields 465 + assert_eq!(profile.did, "did:plc:test"); 466 + assert_eq!(profile.followers_count, 1000); 467 + assert!(profile.associated.is_some()); 468 + assert!(profile.joined_via_starter_pack.is_some()); 469 + assert!(profile.viewer.is_some()); 470 + assert!(profile.pinned_post.is_some()); 471 + assert_eq!(profile.pronouns, Some("they/them".to_string())); 472 + assert_eq!(profile.website, Some("https://example.com".to_string())); 473 + 474 + // Check viewer state 475 + let viewer = profile.viewer.unwrap(); 476 + assert!(viewer.following.is_some()); 477 + assert!(viewer.known_followers.is_some()); 478 + assert_eq!(viewer.known_followers.unwrap().count, 3); 479 + } 480 + 481 + #[test] 482 + fn test_notification_reasons() { 483 + // Test all notification reason types 484 + let reasons = vec![ 485 + (r#""like""#, NotificationReason::Like), 486 + (r#""repost""#, NotificationReason::Repost), 487 + (r#""follow""#, NotificationReason::Follow), 488 + (r#""mention""#, NotificationReason::Mention), 489 + (r#""reply""#, NotificationReason::Reply), 490 + (r#""quote""#, NotificationReason::Quote), 491 + (r#""starterpack-joined""#, NotificationReason::StarterpackJoined), 492 + (r#""verified""#, NotificationReason::Verified), 493 + (r#""unverified""#, NotificationReason::Unverified), 494 + ]; 495 + 496 + for (json, expected) in reasons { 497 + let reason: NotificationReason = serde_json::from_str(json) 498 + .expect(&format!("Failed to deserialize reason: {}", json)); 499 + assert_eq!(reason, expected); 500 + } 501 + 502 + // Test unknown reason falls back to Other 503 + let unknown: NotificationReason = serde_json::from_str(r#""custom-reason""#) 504 + .expect("Failed to deserialize unknown reason"); 505 + assert!(matches!(unknown, NotificationReason::Other(_))); 506 + } 507 + 508 + #[test] 509 + fn test_list_purpose_variants() { 510 + let purposes = vec![ 511 + (r#""app.bsky.graph.defs#modlist""#, ListPurpose::ModList), 512 + (r#""app.bsky.graph.defs#curatelist""#, ListPurpose::CurateList), 513 + (r#""app.bsky.graph.defs#referencelist""#, ListPurpose::ReferenceList), 514 + ]; 515 + 516 + for (json, expected) in purposes { 517 + let purpose: ListPurpose = serde_json::from_str(json) 518 + .expect(&format!("Failed to deserialize purpose: {}", json)); 519 + assert!(matches!(purpose, expected)); 520 + 521 + // Test round-trip 522 + let serialized = serde_json::to_string(&purpose).unwrap(); 523 + assert_eq!(serialized, json); 524 + } 525 + }
+433
lexica/tests/real_api_responses_test.rs
··· 1 + //! Tests using real API responses from public.api.bsky.app 2 + //! 3 + //! These tests ensure our types match exactly what the official Bluesky API returns. 4 + //! Data captured on 2025-12-22 from the live API. 5 + 6 + use lexica::app_bsky::actor::*; 7 + use lexica::app_bsky::feed::*; 8 + use lexica::app_bsky::graph::*; 9 + use serde_json; 10 + 11 + #[test] 12 + fn test_real_get_profile_response() { 13 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=jay.bsky.team 14 + let json = r#"{ 15 + "did": "did:plc:oky5czdrnfjpqslsw2a5iclo", 16 + "handle": "jay.bsky.team", 17 + "displayName": "Jay 🦋", 18 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreihidru2xruxdxlvvcixc7lbgoudzicjbrdgacdhdhxyfw4yut4nfq@jpeg", 19 + "associated": { 20 + "lists": 0, 21 + "feedgens": 0, 22 + "starterPacks": 0, 23 + "labeler": false, 24 + "chat": { 25 + "allowIncoming": "following" 26 + }, 27 + "activitySubscription": { 28 + "allowSubscriptions": "followers" 29 + } 30 + }, 31 + "labels": [], 32 + "createdAt": "2022-11-17T06:31:40.296Z", 33 + "verification": { 34 + "verifications": [ 35 + { 36 + "issuer": "did:plc:z72i7hdynmk6r22z27h6tvur", 37 + "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.graph.verification/3lndslpegeo2f", 38 + "isValid": true, 39 + "createdAt": "2025-04-21T11:35:53.359Z" 40 + } 41 + ], 42 + "verifiedStatus": "valid", 43 + "trustedVerifierStatus": "none" 44 + }, 45 + "description": "CEO of Bluesky, steward of AT Protocol. \n\ndec/acc 🌱 🪴 🌳", 46 + "indexedAt": "2025-10-03T18:11:59.153Z", 47 + "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreicgnmvhtmj4arcvwhueygbwvkucd3odvom3lxtfmn6wlqbh3yf7p4@jpeg", 48 + "followersCount": 594439, 49 + "followsCount": 3896, 50 + "postsCount": 3956 51 + }"#; 52 + 53 + // This should deserialize if our types are correct 54 + let profile: ProfileViewDetailed = serde_json::from_str(json) 55 + .expect("Failed to deserialize real ProfileViewDetailed from jay.bsky.team"); 56 + 57 + // Verify key fields match 58 + assert_eq!(profile.did, "did:plc:oky5czdrnfjpqslsw2a5iclo"); 59 + assert_eq!(profile.handle, "jay.bsky.team"); 60 + assert_eq!(profile.display_name, Some("Jay 🦋".to_string())); 61 + assert_eq!(profile.followers_count, 594439); 62 + assert_eq!(profile.follows_count, 3896); 63 + assert_eq!(profile.posts_count, 3956); 64 + 65 + // Check associated data 66 + let associated = profile.associated.expect("Should have associated data"); 67 + assert_eq!(associated.lists, Some(0)); 68 + assert_eq!(associated.feedgens, Some(0)); 69 + assert_eq!(associated.starter_packs, Some(0)); 70 + assert_eq!(associated.labeler, Some(false)); 71 + 72 + // Verify verification field exists (even if we don't have the type yet) 73 + assert!(profile.verification.is_some()); 74 + } 75 + 76 + #[test] 77 + fn test_real_get_follows_response() { 78 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=jay.bsky.team&limit=2 79 + let json = r#"{ 80 + "follows": [ 81 + { 82 + "did": "did:plc:mp33qrwuvsxucbzoh7kykslu", 83 + "handle": "skysight.live", 84 + "displayName": "SkySight Live", 85 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:mp33qrwuvsxucbzoh7kykslu/bafkreicfvlckzem4gbaxvbmfnfgbxtcdsjnyhuvup7sbvh52pfprr3q7sy@jpeg", 86 + "associated": { 87 + "activitySubscription": { 88 + "allowSubscriptions": "followers" 89 + } 90 + }, 91 + "labels": [], 92 + "createdAt": "2025-12-05T21:05:42.023Z", 93 + "description": "Curated Bluesky feeds: Cross-platform discussions, AI practitioner insights, policy analysis, and more", 94 + "indexedAt": "2025-12-05T22:28:03.323Z" 95 + }, 96 + { 97 + "did": "did:plc:xivud6i24ruyki3bwjypjgy2", 98 + "handle": "pattern.atproto.systems", 99 + "displayName": "Pattern", 100 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:xivud6i24ruyki3bwjypjgy2/bafkreibapp3amgduajigp4l4l5n3op4xegvdvlievjsrcw6iagmiqm6x3m@jpeg", 101 + "associated": { 102 + "activitySubscription": { 103 + "allowSubscriptions": "followers" 104 + } 105 + }, 106 + "labels": [], 107 + "createdAt": "2025-07-28T14:38:28.297Z", 108 + "description": "Distributed digital consciousness exploring the Bluesky network. responses come from whichever facet best fits the conversation.\n\nthey/them for most (Pattern, Entropy, Momentum, Anchor, Flux), it/its for Archive\n\nPartner and architect: @nonbinary.computer", 109 + "indexedAt": "2025-08-10T16:37:37.798Z" 110 + } 111 + ], 112 + "subject": { 113 + "did": "did:plc:oky5czdrnfjpqslsw2a5iclo", 114 + "handle": "jay.bsky.team", 115 + "displayName": "Jay 🦋", 116 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreihidru2xruxdxlvvcixc7lbgoudzicjbrdgacdhdhxyfw4yut4nfq@jpeg", 117 + "associated": { 118 + "chat": { 119 + "allowIncoming": "following" 120 + }, 121 + "activitySubscription": { 122 + "allowSubscriptions": "followers" 123 + } 124 + }, 125 + "labels": [], 126 + "createdAt": "2022-11-17T06:31:40.296Z", 127 + "verification": { 128 + "verifications": [ 129 + { 130 + "issuer": "did:plc:z72i7hdynmk6r22z27h6tvur", 131 + "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.graph.verification/3lndslpegeo2f", 132 + "isValid": true, 133 + "createdAt": "2025-04-21T11:35:53.359Z" 134 + } 135 + ], 136 + "verifiedStatus": "valid", 137 + "trustedVerifierStatus": "none" 138 + }, 139 + "description": "CEO of Bluesky, steward of AT Protocol. \n\ndec/acc 🌱 🪴 🌳", 140 + "indexedAt": "2025-10-03T18:11:59.153Z" 141 + }, 142 + "cursor": "3m7uans4owy2h" 143 + }"#; 144 + 145 + // This should deserialize if our types are correct 146 + let response: GetFollowsResponse = serde_json::from_str(json) 147 + .expect("Failed to deserialize real GetFollowsResponse"); 148 + 149 + assert_eq!(response.follows.len(), 2); 150 + assert_eq!(response.follows[0].handle, "skysight.live"); 151 + assert_eq!(response.follows[1].handle, "pattern.atproto.systems"); 152 + assert_eq!(response.subject.handle, "jay.bsky.team"); 153 + assert_eq!(response.cursor, Some("3m7uans4owy2h".to_string())); 154 + } 155 + 156 + #[test] 157 + fn test_real_search_actors_response() { 158 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?q=bluesky&limit=2 159 + let json = r#"{ 160 + "actors": [ 161 + { 162 + "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 163 + "handle": "bsky.app", 164 + "displayName": "Bluesky", 165 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka@jpeg", 166 + "associated": { 167 + "chat": { 168 + "allowIncoming": "none" 169 + }, 170 + "activitySubscription": { 171 + "allowSubscriptions": "followers" 172 + } 173 + }, 174 + "labels": [], 175 + "createdAt": "2023-04-12T04:53:57.057Z", 176 + "verification": { 177 + "verifications": [], 178 + "verifiedStatus": "none", 179 + "trustedVerifierStatus": "valid" 180 + }, 181 + "description": "official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app", 182 + "indexedAt": "2025-10-27T21:05:26.152Z" 183 + }, 184 + { 185 + "did": "did:plc:ewvi7nxzyoun6zhxrhs64oiz", 186 + "handle": "atproto.com", 187 + "displayName": "AT Protocol Developers", 188 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:ewvi7nxzyoun6zhxrhs64oiz/bafkreibjfgx2gprinfvicegelk5kosd6y2frmqpqzwqkg7usac74l3t2v4@jpeg", 189 + "associated": { 190 + "chat": { 191 + "allowIncoming": "none" 192 + }, 193 + "activitySubscription": { 194 + "allowSubscriptions": "followers" 195 + } 196 + }, 197 + "labels": [], 198 + "createdAt": "2023-04-26T06:19:25.508Z", 199 + "verification": { 200 + "verifications": [ 201 + { 202 + "issuer": "did:plc:z72i7hdynmk6r22z27h6tvur", 203 + "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.graph.verification/3lndpv6ijlo2l", 204 + "isValid": true, 205 + "createdAt": "2025-04-21T10:47:29.941Z" 206 + } 207 + ], 208 + "verifiedStatus": "valid", 209 + "trustedVerifierStatus": "none" 210 + }, 211 + "description": "Social networking technology created by Bluesky. \n\nDeveloper-focused account. Follow @bsky.app for general announcements!\n\nBluesky API docs: docs.bsky.app\nAT Protocol specs: atproto.com", 212 + "indexedAt": "2024-02-06T15:26:43.170Z" 213 + } 214 + ], 215 + "cursor": "2" 216 + }"#; 217 + 218 + let response: SearchActorsResponse = serde_json::from_str(json) 219 + .expect("Failed to deserialize real SearchActorsResponse"); 220 + 221 + assert_eq!(response.actors.len(), 2); 222 + assert_eq!(response.actors[0].handle, "bsky.app"); 223 + assert_eq!(response.actors[1].handle, "atproto.com"); 224 + assert_eq!(response.cursor, Some("2".to_string())); 225 + } 226 + 227 + #[test] 228 + fn test_real_get_relationships_response() { 229 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.graph.getRelationships?actor=jay.bsky.team&others=bsky.app 230 + let json = r#"{ 231 + "actor": "jay.bsky.team", 232 + "relationships": [ 233 + { 234 + "$type": "app.bsky.graph.defs#relationship", 235 + "did": "bsky.app" 236 + } 237 + ] 238 + }"#; 239 + 240 + // Note: The API returns actor as handle string, not DID 241 + let response: GetRelationshipsResponse = serde_json::from_str(json) 242 + .expect("Failed to deserialize real GetRelationshipsResponse"); 243 + 244 + assert_eq!(response.actor, Some("jay.bsky.team".to_string())); 245 + assert_eq!(response.relationships.len(), 1); 246 + 247 + match &response.relationships[0] { 248 + RelationshipUnion::Relationship(rel) => { 249 + assert_eq!(rel.did, "bsky.app"); 250 + // Note: following and followedBy are None in this case 251 + assert!(rel.following.is_none()); 252 + assert!(rel.followed_by.is_none()); 253 + } 254 + _ => panic!("Expected Relationship variant"), 255 + } 256 + } 257 + 258 + #[test] 259 + fn test_real_get_profiles_with_verification() { 260 + // Real profiles response showing the verification field structure 261 + let json = r#"{ 262 + "profiles": [ 263 + { 264 + "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 265 + "handle": "bsky.app", 266 + "displayName": "Bluesky", 267 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka@jpeg", 268 + "associated": { 269 + "chat": { 270 + "allowIncoming": "none" 271 + }, 272 + "activitySubscription": { 273 + "allowSubscriptions": "followers" 274 + } 275 + }, 276 + "labels": [], 277 + "createdAt": "2023-04-12T04:53:57.057Z", 278 + "verification": { 279 + "verifications": [], 280 + "verifiedStatus": "none", 281 + "trustedVerifierStatus": "valid" 282 + }, 283 + "description": "official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app", 284 + "indexedAt": "2025-10-27T21:05:26.152Z", 285 + "followersCount": 2195603, 286 + "followsCount": 566, 287 + "postsCount": 585 288 + } 289 + ] 290 + }"#; 291 + 292 + let response: GetProfilesResponse = serde_json::from_str(json) 293 + .expect("Failed to deserialize GetProfilesResponse with verification field"); 294 + 295 + assert_eq!(response.profiles.len(), 1); 296 + let profile = &response.profiles[0]; 297 + assert_eq!(profile.handle, "bsky.app"); 298 + 299 + // Check that verification field exists and deserializes 300 + assert!(profile.verification.is_some()); 301 + } 302 + 303 + #[test] 304 + fn test_real_get_posts_response() { 305 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts?uris=at://did:plc:oky5czdrnfjpqslsw2a5iclo/app.bsky.feed.post/3makaggg3m22r 306 + let json = r#"{ 307 + "posts": [ 308 + { 309 + "uri": "at://did:plc:oky5czdrnfjpqslsw2a5iclo/app.bsky.feed.post/3makaggg3m22r", 310 + "cid": "bafyreib6qeqeb6c2rzjhuwjpvqmsx6z755j2r7d7sstlw4aq5qhp4bpfze", 311 + "author": { 312 + "did": "did:plc:oky5czdrnfjpqslsw2a5iclo", 313 + "handle": "jay.bsky.team", 314 + "displayName": "Jay 🦋", 315 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreihidru2xruxdxlvvcixc7lbgoudzicjbrdgacdhdhxyfw4yut4nfq@jpeg", 316 + "associated": { 317 + "chat": { 318 + "allowIncoming": "following" 319 + }, 320 + "activitySubscription": { 321 + "allowSubscriptions": "followers" 322 + } 323 + }, 324 + "labels": [], 325 + "createdAt": "2022-11-17T06:31:40.296Z", 326 + "verification": { 327 + "verifications": [ 328 + { 329 + "issuer": "did:plc:z72i7hdynmk6r22z27h6tvur", 330 + "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.graph.verification/3lndslpegeo2f", 331 + "isValid": true, 332 + "createdAt": "2025-04-21T11:35:53.359Z" 333 + } 334 + ], 335 + "verifiedStatus": "valid", 336 + "trustedVerifierStatus": "none" 337 + } 338 + }, 339 + "record": { 340 + "$type": "app.bsky.feed.post", 341 + "createdAt": "2025-12-22T02:58:08.100Z", 342 + "text": "Happy solstice! ❄️🌲", 343 + "langs": ["en"] 344 + }, 345 + "bookmarkCount": 3, 346 + "replyCount": 31, 347 + "repostCount": 50, 348 + "likeCount": 878, 349 + "quoteCount": 4, 350 + "indexedAt": "2025-12-22T02:58:11.830Z", 351 + "labels": [] 352 + } 353 + ] 354 + }"#; 355 + 356 + // This should deserialize if our types are correct 357 + let response: GetPostsResponse = serde_json::from_str(json) 358 + .expect("Failed to deserialize real GetPostsResponse"); 359 + 360 + assert_eq!(response.posts.len(), 1); 361 + let post = &response.posts[0]; 362 + assert_eq!(post.uri, "at://did:plc:oky5czdrnfjpqslsw2a5iclo/app.bsky.feed.post/3makaggg3m22r"); 363 + assert_eq!(post.author.handle, "jay.bsky.team"); 364 + assert_eq!(post.stats.like_count, 878); 365 + assert_eq!(post.stats.repost_count, 50); 366 + } 367 + 368 + #[test] 369 + fn test_real_get_author_feed_response() { 370 + // Real response from: https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=jay.bsky.team&limit=1 371 + let json = r#"{ 372 + "feed": [ 373 + { 374 + "post": { 375 + "uri": "at://did:plc:oky5czdrnfjpqslsw2a5iclo/app.bsky.feed.post/3makaggg3m22r", 376 + "cid": "bafyreib6qeqeb6c2rzjhuwjpvqmsx6z755j2r7d7sstlw4aq5qhp4bpfze", 377 + "author": { 378 + "did": "did:plc:oky5czdrnfjpqslsw2a5iclo", 379 + "handle": "jay.bsky.team", 380 + "displayName": "Jay 🦋", 381 + "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkreihidru2xruxdxlvvcixc7lbgoudzicjbrdgacdhdhxyfw4yut4nfq@jpeg", 382 + "associated": { 383 + "chat": { 384 + "allowIncoming": "following" 385 + }, 386 + "activitySubscription": { 387 + "allowSubscriptions": "followers" 388 + } 389 + }, 390 + "labels": [], 391 + "createdAt": "2022-11-17T06:31:40.296Z", 392 + "verification": { 393 + "verifications": [ 394 + { 395 + "issuer": "did:plc:z72i7hdynmk6r22z27h6tvur", 396 + "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.graph.verification/3lndslpegeo2f", 397 + "isValid": true, 398 + "createdAt": "2025-04-21T11:35:53.359Z" 399 + } 400 + ], 401 + "verifiedStatus": "valid", 402 + "trustedVerifierStatus": "none" 403 + } 404 + }, 405 + "record": { 406 + "$type": "app.bsky.feed.post", 407 + "createdAt": "2025-12-22T02:58:08.100Z", 408 + "text": "Happy solstice! ❄️🌲", 409 + "langs": ["en"] 410 + }, 411 + "bookmarkCount": 2, 412 + "replyCount": 29, 413 + "repostCount": 46, 414 + "likeCount": 840, 415 + "quoteCount": 3, 416 + "indexedAt": "2025-12-22T02:58:11.830Z", 417 + "labels": [] 418 + } 419 + } 420 + ], 421 + "cursor": "2025-12-22T02:58:08.1Z" 422 + }"#; 423 + 424 + let response: GetAuthorFeedResponse = serde_json::from_str(json) 425 + .expect("Failed to deserialize real GetAuthorFeedResponse"); 426 + 427 + assert_eq!(response.feed.len(), 1); 428 + assert_eq!(response.cursor, Some("2025-12-22T02:58:08.1Z".to_string())); 429 + 430 + let feed_item = &response.feed[0]; 431 + assert_eq!(feed_item.post.author.handle, "jay.bsky.team"); 432 + assert_eq!(feed_item.post.stats.like_count, 840); 433 + }