Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 522 lines 16 kB view raw
1/// Integration tests for viewer state fields 2/// 3/// Verifies that viewer fields show the authenticated viewer's relationship 4/// to records (e.g., viewer's like on a gallery) 5import database/repositories/lexicons 6import database/repositories/records 7import gleam/http 8import gleam/json 9import gleam/option.{None} 10import gleam/string 11import gleeunit/should 12import handlers/graphql as graphql_handler 13import lib/oauth/did_cache 14import test_helpers 15import wisp 16import wisp/simulate 17 18// Gallery lexicon with subject field for favorites 19fn create_gallery_lexicon() -> String { 20 json.object([ 21 #("lexicon", json.int(1)), 22 #("id", json.string("social.grain.gallery")), 23 #( 24 "defs", 25 json.object([ 26 #( 27 "main", 28 json.object([ 29 #("type", json.string("record")), 30 #("key", json.string("tid")), 31 #( 32 "record", 33 json.object([ 34 #("type", json.string("object")), 35 #( 36 "required", 37 json.array([json.string("title")], of: fn(x) { x }), 38 ), 39 #( 40 "properties", 41 json.object([ 42 #("title", json.object([#("type", json.string("string"))])), 43 ]), 44 ), 45 ]), 46 ), 47 ]), 48 ), 49 ]), 50 ), 51 ]) 52 |> json.to_string 53} 54 55// Favorite lexicon with AT-URI subject field 56fn create_favorite_lexicon() -> String { 57 json.object([ 58 #("lexicon", json.int(1)), 59 #("id", json.string("social.grain.favorite")), 60 #( 61 "defs", 62 json.object([ 63 #( 64 "main", 65 json.object([ 66 #("type", json.string("record")), 67 #("key", json.string("tid")), 68 #( 69 "record", 70 json.object([ 71 #("type", json.string("object")), 72 #( 73 "required", 74 json.array([json.string("subject")], of: fn(x) { x }), 75 ), 76 #( 77 "properties", 78 json.object([ 79 #( 80 "subject", 81 json.object([ 82 #("type", json.string("string")), 83 #("format", json.string("at-uri")), 84 ]), 85 ), 86 ]), 87 ), 88 ]), 89 ), 90 ]), 91 ), 92 ]), 93 ), 94 ]) 95 |> json.to_string 96} 97 98// Follow lexicon with DID subject field 99fn create_follow_lexicon() -> String { 100 json.object([ 101 #("lexicon", json.int(1)), 102 #("id", json.string("social.grain.graph.follow")), 103 #( 104 "defs", 105 json.object([ 106 #( 107 "main", 108 json.object([ 109 #("type", json.string("record")), 110 #("key", json.string("tid")), 111 #( 112 "record", 113 json.object([ 114 #("type", json.string("object")), 115 #( 116 "required", 117 json.array([json.string("subject")], of: fn(x) { x }), 118 ), 119 #( 120 "properties", 121 json.object([ 122 #( 123 "subject", 124 json.object([ 125 #("type", json.string("string")), 126 #("format", json.string("did")), 127 ]), 128 ), 129 ]), 130 ), 131 ]), 132 ), 133 ]), 134 ), 135 ]), 136 ), 137 ]) 138 |> json.to_string 139} 140 141/// Test: Viewer favorite field returns null when not favorited 142pub fn viewer_favorite_null_when_not_favorited_test() { 143 // Setup database 144 let assert Ok(exec) = test_helpers.create_test_db() 145 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 146 let assert Ok(_) = test_helpers.create_record_table(exec) 147 let assert Ok(_) = test_helpers.create_config_table(exec) 148 let assert Ok(_) = test_helpers.create_actor_table(exec) 149 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 150 let assert Ok(_) = 151 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer") 152 153 // Insert lexicons 154 let assert Ok(_) = 155 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon()) 156 let assert Ok(_) = 157 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon()) 158 159 // Create a gallery record 160 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1" 161 let gallery_json = 162 json.object([#("title", json.string("Test Gallery"))]) 163 |> json.to_string 164 165 let assert Ok(_) = 166 records.insert( 167 exec, 168 gallery_uri, 169 "cid1", 170 "did:plc:author", 171 "social.grain.gallery", 172 gallery_json, 173 ) 174 175 // Query with auth token - should show null for viewerSocialGrainFavoriteViaSubject 176 let query = 177 json.object([ 178 #( 179 "query", 180 json.string( 181 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri } } } } }", 182 ), 183 ), 184 ]) 185 |> json.to_string 186 187 let request = 188 simulate.request(http.Post, "/graphql") 189 |> simulate.string_body(query) 190 |> simulate.header("content-type", "application/json") 191 |> simulate.header("authorization", "Bearer test-viewer-token") 192 193 let assert Ok(cache) = did_cache.start() 194 let response = 195 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "") 196 197 let assert wisp.Text(body) = response.body 198 199 // Debug: print the response if not 200 200 case response.status { 201 200 -> Nil 202 _ -> { 203 // Print error for debugging 204 should.fail() 205 } 206 } 207 208 // Should contain the gallery URI 209 string.contains(body, gallery_uri) |> should.be_true 210 211 // The viewer field should be null since there's no favorite 212 // JSON formatting may have spaces, so check for the field and null value 213 string.contains(body, "viewerSocialGrainFavoriteViaSubject") 214 |> should.be_true 215 string.contains(body, "null") 216 |> should.be_true 217} 218 219/// Test: Schema includes viewer favorite field and query succeeds 220pub fn viewer_favorite_schema_test() { 221 // Setup database 222 let assert Ok(exec) = test_helpers.create_test_db() 223 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 224 let assert Ok(_) = test_helpers.create_record_table(exec) 225 let assert Ok(_) = test_helpers.create_config_table(exec) 226 let assert Ok(_) = test_helpers.create_actor_table(exec) 227 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 228 let assert Ok(_) = 229 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer") 230 231 // Insert lexicons 232 let assert Ok(_) = 233 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon()) 234 let assert Ok(_) = 235 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon()) 236 237 // Create a gallery record 238 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1" 239 let gallery_json = 240 json.object([#("title", json.string("Test Gallery"))]) 241 |> json.to_string 242 243 let assert Ok(_) = 244 records.insert( 245 exec, 246 gallery_uri, 247 "cid1", 248 "did:plc:author", 249 "social.grain.gallery", 250 gallery_json, 251 ) 252 253 // Query WITH viewer field (verifies schema includes it) 254 let query = 255 json.object([ 256 #( 257 "query", 258 json.string( 259 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri subject } } } } }", 260 ), 261 ), 262 ]) 263 |> json.to_string 264 265 let request = 266 simulate.request(http.Post, "/graphql") 267 |> simulate.string_body(query) 268 |> simulate.header("content-type", "application/json") 269 |> simulate.header("authorization", "Bearer test-viewer-token") 270 271 let assert Ok(cache) = did_cache.start() 272 let response = 273 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "") 274 275 let assert wisp.Text(body) = response.body 276 277 // Query should succeed (schema generation works) 278 response.status |> should.equal(200) 279 280 // Should contain the gallery URI 281 string.contains(body, gallery_uri) |> should.be_true 282 283 // Viewer field should exist in response (currently null, data lookup needs work) 284 string.contains(body, "viewerSocialGrainFavoriteViaSubject") 285 |> should.be_true 286} 287 288/// Test: Viewer follow field returns null when not following 289pub fn viewer_follow_null_when_not_following_test() { 290 // Setup database 291 let assert Ok(exec) = test_helpers.create_test_db() 292 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 293 let assert Ok(_) = test_helpers.create_record_table(exec) 294 let assert Ok(_) = test_helpers.create_config_table(exec) 295 let assert Ok(_) = test_helpers.create_actor_table(exec) 296 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 297 let assert Ok(_) = 298 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer") 299 300 // Insert lexicons 301 let assert Ok(_) = 302 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon()) 303 let assert Ok(_) = 304 lexicons.insert(exec, "social.grain.graph.follow", create_follow_lexicon()) 305 306 // Create a gallery record by a different author 307 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1" 308 let gallery_json = 309 json.object([#("title", json.string("Author's Gallery"))]) 310 |> json.to_string 311 312 let assert Ok(_) = 313 records.insert( 314 exec, 315 gallery_uri, 316 "cid1", 317 "did:plc:author", 318 "social.grain.gallery", 319 gallery_json, 320 ) 321 322 // Query with auth token - should show null for viewerSocialGrainGraphFollowViaSubject 323 let query = 324 json.object([ 325 #( 326 "query", 327 json.string( 328 "{ socialGrainGallery { edges { node { uri did viewerSocialGrainGraphFollowViaSubject { uri } } } } }", 329 ), 330 ), 331 ]) 332 |> json.to_string 333 334 let request = 335 simulate.request(http.Post, "/graphql") 336 |> simulate.string_body(query) 337 |> simulate.header("content-type", "application/json") 338 |> simulate.header("authorization", "Bearer test-viewer-token") 339 340 let assert Ok(cache) = did_cache.start() 341 let response = 342 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "") 343 344 let assert wisp.Text(body) = response.body 345 346 response.status |> should.equal(200) 347 348 // Should contain the gallery URI 349 string.contains(body, gallery_uri) |> should.be_true 350 351 // The viewer follow field should be null since viewer doesn't follow the author 352 string.contains(body, "viewerSocialGrainGraphFollowViaSubject") 353 |> should.be_true 354 string.contains(body, "null") 355 |> should.be_true 356} 357 358/// Test: Viewer follow field returns follow when viewer follows the author 359pub fn viewer_follow_returns_follow_when_following_test() { 360 // Setup database 361 let assert Ok(exec) = test_helpers.create_test_db() 362 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 363 let assert Ok(_) = test_helpers.create_record_table(exec) 364 let assert Ok(_) = test_helpers.create_config_table(exec) 365 let assert Ok(_) = test_helpers.create_actor_table(exec) 366 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 367 let assert Ok(_) = 368 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer") 369 370 // Insert lexicons 371 let assert Ok(_) = 372 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon()) 373 let assert Ok(_) = 374 lexicons.insert(exec, "social.grain.graph.follow", create_follow_lexicon()) 375 376 // Create a gallery record by a different author 377 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1" 378 let gallery_json = 379 json.object([#("title", json.string("Author's Gallery"))]) 380 |> json.to_string 381 382 let assert Ok(_) = 383 records.insert( 384 exec, 385 gallery_uri, 386 "cid1", 387 "did:plc:author", 388 "social.grain.gallery", 389 gallery_json, 390 ) 391 392 // Create a follow record from the viewer to the author 393 let follow_uri = "at://did:plc:viewer/social.grain.graph.follow/follow1" 394 let follow_json = 395 json.object([#("subject", json.string("did:plc:author"))]) 396 |> json.to_string 397 398 let assert Ok(_) = 399 records.insert( 400 exec, 401 follow_uri, 402 "cid2", 403 "did:plc:viewer", 404 "social.grain.graph.follow", 405 follow_json, 406 ) 407 408 // Query with auth token only (no variables) 409 let query = 410 json.object([ 411 #( 412 "query", 413 json.string( 414 "{ socialGrainGallery { edges { node { uri did viewerSocialGrainGraphFollowViaSubject { uri } } } } }", 415 ), 416 ), 417 ]) 418 |> json.to_string 419 420 let request = 421 simulate.request(http.Post, "/graphql") 422 |> simulate.string_body(query) 423 |> simulate.header("content-type", "application/json") 424 |> simulate.header("authorization", "Bearer test-viewer-token") 425 426 let assert Ok(cache) = did_cache.start() 427 let response = 428 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "") 429 430 let assert wisp.Text(body) = response.body 431 432 response.status |> should.equal(200) 433 434 // Should contain the gallery URI 435 string.contains(body, gallery_uri) |> should.be_true 436 437 // The viewer follow field should contain the follow URI 438 string.contains(body, follow_uri) |> should.be_true 439} 440 441/// Test: Viewer favorite field returns favorite when viewer has favorited (AT-URI subject) 442pub fn viewer_favorite_returns_favorite_when_favorited_test() { 443 // Setup database 444 let assert Ok(exec) = test_helpers.create_test_db() 445 let assert Ok(_) = test_helpers.create_lexicon_table(exec) 446 let assert Ok(_) = test_helpers.create_record_table(exec) 447 let assert Ok(_) = test_helpers.create_config_table(exec) 448 let assert Ok(_) = test_helpers.create_actor_table(exec) 449 let assert Ok(_) = test_helpers.create_oauth_tables(exec) 450 let assert Ok(_) = 451 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer") 452 453 // Insert lexicons 454 let assert Ok(_) = 455 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon()) 456 let assert Ok(_) = 457 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon()) 458 459 // Create a gallery record 460 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1" 461 let gallery_json = 462 json.object([#("title", json.string("Test Gallery"))]) 463 |> json.to_string 464 465 let assert Ok(_) = 466 records.insert( 467 exec, 468 gallery_uri, 469 "cid1", 470 "did:plc:author", 471 "social.grain.gallery", 472 gallery_json, 473 ) 474 475 // Create a favorite record from the viewer for the gallery 476 let favorite_uri = "at://did:plc:viewer/social.grain.favorite/fav1" 477 let favorite_json = 478 json.object([#("subject", json.string(gallery_uri))]) 479 |> json.to_string 480 481 let assert Ok(_) = 482 records.insert( 483 exec, 484 favorite_uri, 485 "cid2", 486 "did:plc:viewer", 487 "social.grain.favorite", 488 favorite_json, 489 ) 490 491 // Query with auth token 492 let query = 493 json.object([ 494 #( 495 "query", 496 json.string( 497 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri subject } } } } }", 498 ), 499 ), 500 ]) 501 |> json.to_string 502 503 let request = 504 simulate.request(http.Post, "/graphql") 505 |> simulate.string_body(query) 506 |> simulate.header("content-type", "application/json") 507 |> simulate.header("authorization", "Bearer test-viewer-token") 508 509 let assert Ok(cache) = did_cache.start() 510 let response = 511 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "") 512 513 let assert wisp.Text(body) = response.body 514 515 response.status |> should.equal(200) 516 517 // Should contain the gallery URI 518 string.contains(body, gallery_uri) |> should.be_true 519 520 // The viewer favorite field should contain the favorite URI 521 string.contains(body, favorite_uri) |> should.be_true 522}