//! Fan+ perks: + badge, signature, and image-embed gating.
//!
//! The plumbing for perks is exercised in `workflows::auth`; these tests focus
//! on the user-visible effects:
//! * Embed gate at submit time for non-plus users
//! * Plus users get image markdown rendered
//! * Signature endpoint refuses non-plus users
//! * + badge and signature show beneath posts of current Fan+ subscribers
//! * Signature hides when the author lapses
use crate::harness::TestHarness;
use axum::http::StatusCode;
use uuid::Uuid;
// ── Helpers ──
/// Log in as `username` and set their denormalised perk flags + (optionally) a
/// pre-rendered signature. The `/_test/login` endpoint also stuffs perks into
/// the session, mirroring what the OAuth callback does.
async fn login_with_perks(
h: &mut TestHarness,
username: &str,
fan_plus: bool,
is_creator: bool,
signature: Option<(&str, &str)>,
) -> Uuid {
let user_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO users (mnw_account_id, username, display_name, is_fan_plus, is_creator) \
VALUES ($1, $2, $2, $3, $4) ON CONFLICT (mnw_account_id) DO UPDATE \
SET is_fan_plus = $3, is_creator = $4",
)
.bind(user_id)
.bind(username)
.bind(fan_plus)
.bind(is_creator)
.execute(&h.db)
.await
.expect("insert user");
if let Some((md, html)) = signature {
sqlx::query("UPDATE users SET signature_markdown = $2, signature_html = $3 WHERE mnw_account_id = $1")
.bind(user_id)
.bind(md)
.bind(html)
.execute(&h.db)
.await
.expect("seed signature");
}
h.client.get("/").await;
h.client
.post_json(
"/_test/login",
&serde_json::json!({
"user_id": user_id.to_string(),
"username": username,
"perks": { "fan_plus": fan_plus, "is_creator": is_creator },
})
.to_string(),
)
.await;
user_id
}
async fn setup_community(h: &TestHarness, user_id: Uuid) -> Uuid {
let comm_id = h.create_community("Test", "test").await;
h.create_category(comm_id, "General", "general").await;
h.add_membership(user_id, comm_id, "member").await;
comm_id
}
// ── Embed gate ──
#[tokio::test]
async fn free_user_image_embed_rejected() {
let mut h = TestHarness::new().await;
let user_id = login_with_perks(&mut h, "freeposter", false, false, None).await;
let _ = setup_community(&h, user_id).await;
h.client.get("/p/test/general/new").await;
let resp = h
.client
.post_form(
"/p/test/general/new",
"title=Picture&body=Look%3A+%21%5Balt%5D%28https%3A%2F%2Fexample.com%2Fa.png%29",
)
.await;
assert_eq!(resp.status, StatusCode::UNPROCESSABLE_ENTITY);
assert!(resp.text.contains("Fan+"));
}
#[tokio::test]
async fn plus_user_image_embed_renders() {
let mut h = TestHarness::new().await;
let user_id = login_with_perks(&mut h, "plusposter", true, false, None).await;
let _ = setup_community(&h, user_id).await;
h.client.get("/p/test/general/new").await;
let resp = h
.client
.post_form(
"/p/test/general/new",
"title=Picture&body=Look%3A+%21%5Balt%5D%28https%3A%2F%2Fexample.com%2Fa.png%29",
)
.await;
assert!(resp.status.is_redirection(), "status: {}", resp.status);
// Confirm the rendered HTML kept the image.
let html: String = sqlx::query_scalar("SELECT body_html FROM posts ORDER BY created_at DESC LIMIT 1")
.fetch_one(&h.db)
.await
.unwrap();
assert!(html.contains(", Option
old
Cheers ~ plusauthor
")), ) .await; let comm_id = setup_community(&h, user_id).await; let cat_id: Uuid = sqlx::query_scalar("SELECT id FROM categories WHERE community_id = $1") .bind(comm_id) .fetch_one(&h.db) .await .unwrap(); let thread_id = h .create_thread_with_post(cat_id, user_id, "Hello", "body") .await; let resp = h.client.get(&format!("/p/test/general/{}", thread_id)).await; assert!(resp.text.contains("badge-plus"), "expected + badge"); assert!( resp.text.contains("Cheers ~ plusauthor"), "expected signature in rendered HTML" ); } #[tokio::test] async fn lapsed_plus_user_signature_hidden() { let mut h = TestHarness::new().await; let user_id = login_with_perks( &mut h, "lapsed", true, false, Some(("Old sig", "Old sig
")), ) .await; let comm_id = setup_community(&h, user_id).await; let cat_id: Uuid = sqlx::query_scalar("SELECT id FROM categories WHERE community_id = $1") .bind(comm_id) .fetch_one(&h.db) .await .unwrap(); let thread_id = h .create_thread_with_post(cat_id, user_id, "Hi", "body") .await; // Simulate Fan+ lapse — denormalised flag flips, signature row preserved. sqlx::query("UPDATE users SET is_fan_plus = FALSE WHERE mnw_account_id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); let resp = h.client.get(&format!("/p/test/general/{}", thread_id)).await; assert!( !resp.text.contains("Old sig"), "lapsed signature should not render" ); assert!( !resp.text.contains("badge-plus"), "lapsed user should not have + badge" ); // Row still on disk — user gets it back on renewal. let md: Option