Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 455 lines 13 kB view raw
1/// End-to-end tests for notifications GraphQL query 2/// 3/// Tests verify that: 4/// - notifications query returns records mentioning the given DID 5/// - Self-authored records are excluded 6/// - Collection filtering works correctly 7/// - Union type resolution works across different record types 8import database/repositories/actors 9import database/repositories/lexicons 10import database/repositories/records 11import gleam/json 12import gleam/option 13import gleam/string 14import gleeunit/should 15import graphql/lexicon/schema as lexicon_schema 16import lib/oauth/did_cache 17import test_helpers 18 19// Helper to create a post lexicon JSON 20fn create_post_lexicon() -> String { 21 json.object([ 22 #("lexicon", json.int(1)), 23 #("id", json.string("app.bsky.feed.post")), 24 #( 25 "defs", 26 json.object([ 27 #( 28 "main", 29 json.object([ 30 #("type", json.string("record")), 31 #("key", json.string("tid")), 32 #( 33 "record", 34 json.object([ 35 #("type", json.string("object")), 36 #( 37 "required", 38 json.array([json.string("text")], of: fn(x) { x }), 39 ), 40 #( 41 "properties", 42 json.object([ 43 #( 44 "text", 45 json.object([ 46 #("type", json.string("string")), 47 #("maxLength", json.int(300)), 48 ]), 49 ), 50 #( 51 "createdAt", 52 json.object([ 53 #("type", json.string("string")), 54 #("format", json.string("datetime")), 55 ]), 56 ), 57 ]), 58 ), 59 ]), 60 ), 61 ]), 62 ), 63 ]), 64 ), 65 ]) 66 |> json.to_string 67} 68 69// Helper to create a like lexicon JSON with subject field 70fn create_like_lexicon() -> String { 71 json.object([ 72 #("lexicon", json.int(1)), 73 #("id", json.string("app.bsky.feed.like")), 74 #( 75 "defs", 76 json.object([ 77 #( 78 "main", 79 json.object([ 80 #("type", json.string("record")), 81 #("key", json.string("tid")), 82 #( 83 "record", 84 json.object([ 85 #("type", json.string("object")), 86 #( 87 "required", 88 json.array([json.string("subject")], of: fn(x) { x }), 89 ), 90 #( 91 "properties", 92 json.object([ 93 #( 94 "subject", 95 json.object([ 96 #("type", json.string("string")), 97 #("format", json.string("at-uri")), 98 ]), 99 ), 100 #( 101 "createdAt", 102 json.object([ 103 #("type", json.string("string")), 104 #("format", json.string("datetime")), 105 ]), 106 ), 107 ]), 108 ), 109 ]), 110 ), 111 ]), 112 ), 113 ]), 114 ), 115 ]) 116 |> json.to_string 117} 118 119// Helper to create a follow lexicon JSON with subject field (DID) 120fn create_follow_lexicon() -> String { 121 json.object([ 122 #("lexicon", json.int(1)), 123 #("id", json.string("app.bsky.graph.follow")), 124 #( 125 "defs", 126 json.object([ 127 #( 128 "main", 129 json.object([ 130 #("type", json.string("record")), 131 #("key", json.string("tid")), 132 #( 133 "record", 134 json.object([ 135 #("type", json.string("object")), 136 #( 137 "required", 138 json.array([json.string("subject")], of: fn(x) { x }), 139 ), 140 #( 141 "properties", 142 json.object([ 143 #( 144 "subject", 145 json.object([#("type", json.string("string"))]), 146 ), 147 #( 148 "createdAt", 149 json.object([ 150 #("type", json.string("string")), 151 #("format", json.string("datetime")), 152 ]), 153 ), 154 ]), 155 ), 156 ]), 157 ), 158 ]), 159 ), 160 ]), 161 ), 162 ]) 163 |> json.to_string 164} 165 166// Test: notifications query returns records mentioning the target DID 167pub fn notifications_returns_mentioning_records_test() { 168 // Setup database 169 let assert Ok(exec) = test_helpers.create_test_db() 170 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 171 let assert Ok(_) = test_helpers.create_record_table(exec) 172 let assert Ok(_) = test_helpers.create_actor_table(exec) 173 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 174 let assert Ok(_) = 175 test_helpers.insert_test_token( 176 exec, 177 "test-notification-token", 178 "did:plc:target", 179 ) 180 181 // Insert lexicons 182 let assert Ok(_) = 183 lexicons.insert(exec, "app.bsky.feed.post", create_post_lexicon()) 184 let assert Ok(_) = 185 lexicons.insert(exec, "app.bsky.feed.like", create_like_lexicon()) 186 let assert Ok(_) = 187 lexicons.insert(exec, "app.bsky.graph.follow", create_follow_lexicon()) 188 189 // Setup actors 190 let assert Ok(_) = actors.upsert(exec, "did:plc:target", "target.bsky.social") 191 let assert Ok(_) = actors.upsert(exec, "did:plc:alice", "alice.bsky.social") 192 let assert Ok(_) = actors.upsert(exec, "did:plc:bob", "bob.bsky.social") 193 194 // Target's own post (should NOT appear in notifications) 195 let assert Ok(_) = 196 records.insert( 197 exec, 198 "at://did:plc:target/app.bsky.feed.post/post1", 199 "bafy001", 200 "did:plc:target", 201 "app.bsky.feed.post", 202 "{\"text\":\"Hello world\",\"createdAt\":\"2024-01-01T00:00:00Z\"}", 203 ) 204 205 // Alice likes target's post (SHOULD appear) 206 let assert Ok(_) = 207 records.insert( 208 exec, 209 "at://did:plc:alice/app.bsky.feed.like/like1", 210 "bafy002", 211 "did:plc:alice", 212 "app.bsky.feed.like", 213 "{\"subject\":\"at://did:plc:target/app.bsky.feed.post/post1\",\"createdAt\":\"2024-01-02T00:00:00Z\"}", 214 ) 215 216 // Bob follows target (SHOULD appear) 217 let assert Ok(_) = 218 records.insert( 219 exec, 220 "at://did:plc:bob/app.bsky.graph.follow/follow1", 221 "bafy003", 222 "did:plc:bob", 223 "app.bsky.graph.follow", 224 "{\"subject\":\"did:plc:target\",\"createdAt\":\"2024-01-03T00:00:00Z\"}", 225 ) 226 227 // Alice's unrelated post (should NOT appear) 228 let assert Ok(_) = 229 records.insert( 230 exec, 231 "at://did:plc:alice/app.bsky.feed.post/post2", 232 "bafy004", 233 "did:plc:alice", 234 "app.bsky.feed.post", 235 "{\"text\":\"Unrelated post\",\"createdAt\":\"2024-01-04T00:00:00Z\"}", 236 ) 237 238 // Query all notifications - verify union type resolution works correctly 239 let query = 240 " 241 query { 242 notifications(first: 10) { 243 edges { 244 cursor 245 node { 246 __typename 247 ... on AppBskyFeedLike { 248 uri 249 } 250 ... on AppBskyGraphFollow { 251 uri 252 } 253 } 254 } 255 pageInfo { 256 hasNextPage 257 hasPreviousPage 258 } 259 } 260 } 261 " 262 263 let assert Ok(cache) = did_cache.start() 264 let assert Ok(response_json) = 265 lexicon_schema.execute_query_with_db( 266 exec, 267 query, 268 "{}", 269 Ok("test-notification-token"), 270 cache, 271 option.None, 272 "", 273 "https://plc.directory", 274 ) 275 276 // Verify union type resolution returns concrete types 277 string.contains(response_json, "AppBskyFeedLike") 278 |> should.be_true 279 280 string.contains(response_json, "AppBskyGraphFollow") 281 |> should.be_true 282 283 // Verify URIs are returned from inline fragments 284 string.contains(response_json, "like1") 285 |> should.be_true 286 287 string.contains(response_json, "follow1") 288 |> should.be_true 289 290 // Should have pagination info 291 string.contains(response_json, "hasNextPage") 292 |> should.be_true 293} 294 295// Test: notifications query with collection filter 296pub fn notifications_filters_by_collection_test() { 297 // Setup database 298 let assert Ok(exec) = test_helpers.create_test_db() 299 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 300 let assert Ok(_) = test_helpers.create_record_table(exec) 301 let assert Ok(_) = test_helpers.create_actor_table(exec) 302 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 303 let assert Ok(_) = 304 test_helpers.insert_test_token( 305 exec, 306 "test-notification-token", 307 "did:plc:target", 308 ) 309 310 // Insert lexicons 311 let assert Ok(_) = 312 lexicons.insert(exec, "app.bsky.feed.post", create_post_lexicon()) 313 let assert Ok(_) = 314 lexicons.insert(exec, "app.bsky.feed.like", create_like_lexicon()) 315 let assert Ok(_) = 316 lexicons.insert(exec, "app.bsky.graph.follow", create_follow_lexicon()) 317 318 // Setup actors 319 let assert Ok(_) = actors.upsert(exec, "did:plc:target", "target.bsky.social") 320 let assert Ok(_) = actors.upsert(exec, "did:plc:alice", "alice.bsky.social") 321 let assert Ok(_) = actors.upsert(exec, "did:plc:bob", "bob.bsky.social") 322 323 // Alice likes target's post 324 let assert Ok(_) = 325 records.insert( 326 exec, 327 "at://did:plc:alice/app.bsky.feed.like/like1", 328 "bafy002", 329 "did:plc:alice", 330 "app.bsky.feed.like", 331 "{\"subject\":\"at://did:plc:target/app.bsky.feed.post/post1\",\"createdAt\":\"2024-01-02T00:00:00Z\"}", 332 ) 333 334 // Bob follows target 335 let assert Ok(_) = 336 records.insert( 337 exec, 338 "at://did:plc:bob/app.bsky.graph.follow/follow1", 339 "bafy003", 340 "did:plc:bob", 341 "app.bsky.graph.follow", 342 "{\"subject\":\"did:plc:target\",\"createdAt\":\"2024-01-03T00:00:00Z\"}", 343 ) 344 345 // Query only likes (not follows) 346 let query = 347 " 348 query { 349 notifications(collections: [APP_BSKY_FEED_LIKE], first: 10) { 350 edges { 351 cursor 352 node { 353 __typename 354 ... on AppBskyFeedLike { 355 uri 356 } 357 } 358 } 359 } 360 } 361 " 362 363 let assert Ok(cache) = did_cache.start() 364 let assert Ok(response_json) = 365 lexicon_schema.execute_query_with_db( 366 exec, 367 query, 368 "{}", 369 Ok("test-notification-token"), 370 cache, 371 option.None, 372 "", 373 "https://plc.directory", 374 ) 375 376 // Should have the like with correct type 377 string.contains(response_json, "AppBskyFeedLike") 378 |> should.be_true 379 380 string.contains(response_json, "like1") 381 |> should.be_true 382 383 // Should NOT have the follow (filtered out) 384 string.contains(response_json, "follow1") 385 |> should.be_false 386 387 string.contains(response_json, "AppBskyGraphFollow") 388 |> should.be_false 389} 390 391// Test: notifications query excludes self-authored records 392pub fn notifications_excludes_self_authored_test() { 393 // Setup database 394 let assert Ok(exec) = test_helpers.create_test_db() 395 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 396 let assert Ok(_) = test_helpers.create_record_table(exec) 397 let assert Ok(_) = test_helpers.create_actor_table(exec) 398 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 399 let assert Ok(_) = 400 test_helpers.insert_test_token( 401 exec, 402 "test-notification-token", 403 "did:plc:target", 404 ) 405 406 // Insert lexicons 407 let assert Ok(_) = 408 lexicons.insert(exec, "app.bsky.feed.post", create_post_lexicon()) 409 410 // Setup actors 411 let assert Ok(_) = actors.upsert(exec, "did:plc:target", "target.bsky.social") 412 413 // Target's own post that mentions themselves (should NOT appear) 414 let assert Ok(_) = 415 records.insert( 416 exec, 417 "at://did:plc:target/app.bsky.feed.post/post1", 418 "bafy001", 419 "did:plc:target", 420 "app.bsky.feed.post", 421 "{\"text\":\"Talking about did:plc:target\",\"createdAt\":\"2024-01-01T00:00:00Z\"}", 422 ) 423 424 let query = 425 " 426 query { 427 notifications(first: 10) { 428 edges { 429 cursor 430 node { 431 __typename 432 } 433 } 434 } 435 } 436 " 437 438 let assert Ok(cache) = did_cache.start() 439 let assert Ok(response_json) = 440 lexicon_schema.execute_query_with_db( 441 exec, 442 query, 443 "{}", 444 Ok("test-notification-token"), 445 cache, 446 option.None, 447 "", 448 "https://plc.directory", 449 ) 450 451 // Should have empty edges since self-authored is excluded 452 // Check for empty edges array (with space after colon) 453 string.contains(response_json, "\"edges\": []") 454 |> should.be_true 455}