//! Follow/unfollow: user follows, project follows, self-follow rejection, nonexistent target. use crate::harness::TestHarness; use serde_json::Value; #[tokio::test] async fn follow_unfollow_user() { let mut h = TestHarness::new().await; // Create two users let user_a = h.signup("follower", "follower@test.com", "password123").await; h.client.post_form("/logout", "").await; let user_b = h.signup("followee", "followee@test.com", "password123").await; h.client.post_form("/logout", "").await; // Login as user A h.login("follower", "password123").await; // Follow user B let resp = h .client .post_form(&format!("/api/follow/user/{}", *user_b), "") .await; assert!( resp.status.is_success(), "Follow user failed: {} {}", resp.status, resp.text ); // Response is HTML (FollowButtonTemplate) — check it contains follow state assert!( resp.text.contains("1") || resp.text.contains("follower_count"), "Follow response should contain follower count" ); // Verify in DB let is_following: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM follows WHERE follower_id = $1 AND target_id = $2)", ) .bind(user_a) .bind(*user_b) .fetch_one(&h.db) .await .unwrap(); assert!(is_following, "Should be following in DB"); // Unfollow user B let resp = h .client .delete(&format!("/api/follow/user/{}", *user_b)) .await; assert!( resp.status.is_success(), "Unfollow user failed: {} {}", resp.status, resp.text ); // Verify unfollowed in DB let is_following: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM follows WHERE follower_id = $1 AND target_id = $2)", ) .bind(user_a) .bind(*user_b) .fetch_one(&h.db) .await .unwrap(); assert!(!is_following, "Should not be following after unfollow"); } #[tokio::test] async fn follow_unfollow_project() { let mut h = TestHarness::new().await; // Create creator with a public project let creator_id = h .signup("projcreator", "projcreator@test.com", "password123") .await; h.grant_creator(creator_id).await; h.client.post_form("/logout", "").await; h.login("projcreator", "password123").await; let resp = h .client .post_form("/api/projects", "slug=followproj&title=Follow+Project") .await; assert!( resp.status.is_success(), "Create project failed: {} {}", resp.status, resp.text ); let project: Value = resp.json(); let project_id = project["id"].as_str().unwrap().to_string(); // Make it public let resp = h .client .put_json( &format!("/api/projects/{}", project_id), r#"{"visibility": "public"}"#, ) .await; assert!( resp.status.is_success(), "Make project public failed: {} {}", resp.status, resp.text ); // Logout and create a different user to follow the project h.client.post_form("/logout", "").await; let follower_id = h .signup("projfollower", "projfollower@test.com", "password123") .await; // Follow the project let resp = h .client .post_form(&format!("/api/follow/project/{}", project_id), "") .await; assert!( resp.status.is_success(), "Follow project failed: {} {}", resp.status, resp.text ); // Verify in DB let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows WHERE follower_id = $1 AND target_id = $2::uuid", ) .bind(follower_id) .bind(&project_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(count, 1, "Should have one follow record"); // Unfollow let resp = h .client .delete(&format!("/api/follow/project/{}", project_id)) .await; assert!( resp.status.is_success(), "Unfollow project failed: {} {}", resp.status, resp.text ); let count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows WHERE follower_id = $1 AND target_id = $2::uuid", ) .bind(follower_id) .bind(&project_id) .fetch_one(&h.db) .await .unwrap(); assert_eq!(count, 0, "Follow record should be deleted"); } #[tokio::test] async fn follow_self_rejected() { let mut h = TestHarness::new().await; let user_id = h .signup("selffollow", "selffollow@test.com", "password123") .await; // Try to follow yourself let resp = h .client .post_form(&format!("/api/follow/user/{}", *user_id), "") .await; assert_eq!( resp.status, 400, "Self-follow should be rejected, got {} {}", resp.status, resp.text ); } #[tokio::test] async fn own_profile_hides_follow_button() { let mut h = TestHarness::new().await; h.signup("selfview", "selfview@test.com", "password123").await; let resp = h.client.get("/u/selfview").await; assert_eq!(resp.status, 200); // Should NOT show a follow/unfollow button assert!( !resp.text.contains("follow-btn"), "Own profile should not show follow button" ); } #[tokio::test] async fn follow_nonexistent_rejected() { let mut h = TestHarness::new().await; let _user_id = h .signup("ghostfollow", "ghostfollow@test.com", "password123") .await; // Try to follow a random UUID that doesn't exist let fake_id = uuid::Uuid::new_v4(); let resp = h .client .post_form(&format!("/api/follow/user/{}", fake_id), "") .await; assert_eq!( resp.status, 404, "Following nonexistent user should 404, got {} {}", resp.status, resp.text ); }