Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316use super::test_support::*;use super::views;
#[tokio::test]async fn get_session_route_returns_unauthenticated_marker() { let state = state().await; let app = crate::appview::router().with_state(state); let resp = app .oneshot( Request::builder() .uri("/xrpc/space.polymodel.actor.getSession") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = body_text(resp).await; assert!(body.contains("\"authenticated\":false"), "body was: {body}");}
#[tokio::test]async fn get_session_in_process_helper_returns_authenticated_viewer() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_profile(&state.pool, DID_A, "Alice").await; let session_data = client_session(DID_A, "in-process-session"); let session = OAuthSession::new( state.oauth.registry.clone(), state.oauth.client.clone(), session_data, );
let output = crate::appview::actor::get_session_output(&state, Some(&session)) .await .unwrap();
match output.value { SessionView::SessionAuthenticated(auth) => { assert!(auth.authenticated); assert_eq!(auth.did, did(DID_A)); assert_eq!(auth.profile.display_name.as_deref(), Some("Alice")); } other => panic!("expected authenticated session, got {other:?}"), }}
#[tokio::test]async fn get_session_route_accepts_x_jacquard_session_header() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_profile(&state.pool, DID_A, "Alice").await; let key = seed_oauth_session(&state, DID_A, "header-session").await; let encoded = jacquard_axum::oauth::encode_session_key(&key).unwrap(); let header_name = state.oauth_config.session_header.clone(); let app = crate::appview::router().with_state(state);
let resp = app .oneshot( Request::builder() .uri("/xrpc/space.polymodel.actor.getSession") .header(header_name, encoded) .body(Body::empty()) .unwrap(), ) .await .unwrap();
assert_eq!(resp.status(), StatusCode::OK); let body = body_text(resp).await; let output: GetSessionOutput = serde_json::from_str(&body).unwrap(); match output.value { SessionView::SessionAuthenticated(auth) => { assert!(auth.authenticated); assert_eq!(auth.did, did(DID_A)); } other => panic!("expected authenticated session from header, got {other:?}"), }}
#[tokio::test]async fn fullstack_context_bridge_returns_authenticated_session_from_header() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_profile(&state.pool, DID_A, "Alice").await; let key = seed_oauth_session(&state, DID_A, "fullstack-header-session").await; let encoded = jacquard_axum::oauth::encode_session_key(&key).unwrap(); let header_name = state.oauth_config.session_header.clone();
let mut parts = Request::builder() .uri("/") .header(header_name, encoded) .body(()) .unwrap() .into_parts() .0; parts.extensions.insert(state);
let output = FullstackContext::new(parts) .scope(crate::appview::ssr::get_session_output_from_fullstack_context()) .await .unwrap();
match output.value { SessionView::SessionAuthenticated(auth) => { assert!(auth.authenticated); assert_eq!(auth.did, did(DID_A)); } other => panic!("expected authenticated session from fullstack bridge, got {other:?}"), }}
#[tokio::test]async fn fullstack_context_feed_helper_supports_following() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_identity(&state.pool, DID_B, "bob.com").await; seed_thing( &state.pool, DID_B, "3bbbbbbbbbbbb", "followed project", &[], 0, ) .await; sqlx::query( "INSERT INTO follows (follower_did, followed_did, rkey, cid, created_at) VALUES (?, ?, ?, ?, ?)", ) .bind(DID_A) .bind(DID_B) .bind("followb") .bind("bafyreifollowb") .bind(1_i64) .execute(&state.pool) .await .unwrap(); let key = seed_oauth_session(&state, DID_A, "fullstack-following-session").await; let encoded = jacquard_axum::oauth::encode_session_key(&key).unwrap(); let header_name = state.oauth_config.session_header.clone();
let mut parts = Request::builder() .uri("/") .header(header_name, encoded) .body(()) .unwrap() .into_parts() .0; parts.extensions.insert(state);
let feed = FullstackContext::new(parts) .scope(crate::appview::ssr::get_feed_from_fullstack_context( "following", 24, )) .await .unwrap();
assert_eq!(feed.items.len(), 1); assert_eq!(feed.items[0].thing.name.as_str(), "followed project");}
#[tokio::test]async fn get_thing_route_decodes_query_and_404s_when_missing() { let state = state().await; let app = crate::appview::router().with_state(state); let resp = app .oneshot( Request::builder() .uri("/xrpc/space.polymodel.library.getThing?uri=at%3A%2F%2Fdid%3Aplc%3Ax%2Fspace.polymodel.library.thing%2Fnone") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND);}
#[tokio::test]async fn unauthenticated_write_routes_return_xrpc_401_not_redirects() { let state = state().await; let app = crate::appview::router().with_state(state); let routes = [ ( "/xrpc/space.polymodel.actor.bootstrapProfile", "application/json", "{}", ), ( "/xrpc/space.polymodel.library.stageFile", "model/stl", "solid cube", ), ( "/xrpc/space.polymodel.graph.createLike", "application/json", r#"{"subject":{"uri":"at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.thing/t1","cid":"bafycid"}}"#, ), ];
for (path, content_type, body) in routes { let resp = app .clone() .oneshot( Request::builder() .method("POST") .uri(path) .header("content-type", content_type) .body(Body::from(body)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED, "route {path}"); assert!( resp.headers().get("location").is_none(), "strict XRPC auth must not redirect for {path}" ); let body = body_text(resp).await; assert!(body.contains("AuthenticationRequired"), "body was: {body}"); }}
#[tokio::test]async fn negative_offset_cursor_is_rejected() { let state = state().await; let errs = [ views::search_things(&state, "x", 10, Some("-1"), None) .await .unwrap_err(), views::list_things( &state, "at://did:plc:l/space.polymodel.graph.list/self", 10, Some("-1"), None, ) .await .unwrap_err(), ]; for err in errs { assert!( matches!(err, AppError::InvalidRequest(_)), "negative offset cursor must be InvalidRequest" ); }}
#[tokio::test]async fn feed_recent_paginates_by_rkey() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_thing(&state.pool, DID_A, "3zzzzzzzzzzzz", "t3", &[], 0).await; seed_thing(&state.pool, DID_A, "3yyyyyyyyyyyy", "t2", &[], 0).await; seed_thing(&state.pool, DID_A, "3xxxxxxxxxxxx", "t1", &[], 0).await;
let page1 = views::feed_recent(&state, 2, None, None).await.unwrap(); assert_eq!(page1.items.len(), 2); assert_eq!(page1.items[0].thing.name.as_str(), "t3"); let cursor = page1.cursor.expect("more remain"); let page2 = views::feed_recent(&state, 2, Some(cursor.as_ref()), None) .await .unwrap(); assert_eq!(page2.items.len(), 1); assert_eq!(page2.items[0].thing.name.as_str(), "t1"); assert!(page2.cursor.is_none());}
#[tokio::test]async fn feed_following_cursor_handles_same_rkey_across_followed_repos() { let state = state().await; seed_identity(&state.pool, DID_A, "alice.com").await; seed_identity(&state.pool, DID_B, "bob.com").await; seed_identity(&state.pool, "did:plc:viewer", "viewer.com").await; seed_thing(&state.pool, DID_A, "3sameeeeeeeee", "Alice thing", &[], 0).await; seed_thing(&state.pool, DID_B, "3sameeeeeeeee", "Bob thing", &[], 0).await; sqlx::query("INSERT INTO follows (follower_did, followed_did, rkey, cid, created_at) VALUES (?, ?, ?, ?, ?)") .bind("did:plc:viewer") .bind(DID_A) .bind("fa") .bind("cid-a") .bind(1_i64) .execute(&state.pool) .await .unwrap(); sqlx::query("INSERT INTO follows (follower_did, followed_did, rkey, cid, created_at) VALUES (?, ?, ?, ?, ?)") .bind("did:plc:viewer") .bind(DID_B) .bind("fb") .bind("cid-b") .bind(2_i64) .execute(&state.pool) .await .unwrap();
let viewer = did("did:plc:viewer"); let page1 = views::feed_following(&state, viewer.borrow(), 1, None) .await .unwrap(); let cursor = page1.cursor.as_deref().expect("first row has another page"); assert!(cursor.starts_with("following:v1:")); let page2 = views::feed_following(&state, viewer.borrow(), 1, Some(cursor)) .await .unwrap();
let names = page1 .items .into_iter() .chain(page2.items.into_iter()) .map(|item| item.thing.name.to_string()) .collect::<std::collections::HashSet<_>>(); assert_eq!(names.len(), 2); assert!(names.contains("Alice thing")); assert!(names.contains("Bob thing"));}